Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Ratelimit by header value creates keys using cached values #66

Closed
caalberts opened this issue Jun 28, 2018 · 8 comments
Closed

Ratelimit by header value creates keys using cached values #66

caalberts opened this issue Jun 28, 2018 · 8 comments

Comments

@caalberts
Copy link

Hello! First of all, thanks for the great library. It worked great, until I had to add rate limiting by user id in the request header.

I came across #43, however I found that the function tollbooth.BuildKeys build keys using existing header values in the cache. Specifically this line:

for _, headerValue := range headerValues {

Should the key be created this way instead of looping through existing values?

sliceKeys = append(sliceKeys, []string{remoteIP, path, r.Method, headerKey, r.Header.Get(headerKey)})

As I understand it, this sliceKeys is then used to lookup against the rate limit cache. If sliceKeys contain existing headers, the current request would get rate limited due to one of the existing headers.

If I have used the package wrongly, please help me to understand how I should use it.

Here are the middleware I wrote and the test.

func RateLimit(h http.HandlerFunc) http.HandlerFunc {
	allocationLimiter := tollbooth.NewLimiter(1, &limiter.ExpirableOptions{DefaultExpirationTTL: time.Minute}).
		SetMethods([]string{"POST"})

	handler := func(w http.ResponseWriter, r *http.Request) {
		customerID := r.Header.Get("X_Owner_ID")
		fmt.Printf("customerID: %s\n", customerID)
		allocationLimiter.SetHeader("X_Owner_ID", []string{customerID})

		tollbooth.LimitFuncHandler(allocationLimiter, h).ServeHTTP(w, r)
	}

	return handler
}
func TestRateLimit(t *testing.T) {
	customerID1 := "1234"
	customerID2 := "5678"

	tests := []struct {
		name                string
		secondRequestStatus int
		customerIDs         []string
	}{
		{"different customer id", http.StatusOK, []string{customerID1, customerID2}},
		{"same customer id", http.StatusTooManyRequests, []string{customerID1, customerID1}},
	}
	for _, tt := range tests {
		t.Run(tt.name, func(t *testing.T) {
			h := http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {})
			testServer := httptest.NewServer(RateLimit(h))
			defer testServer.Close()
			client := &http.Client{}

			request1, err := http.NewRequest("POST", testServer.URL, nil)
			assert.Nil(t, err)
			request1.Header.Add("CustomerID", tt.customerIDs[0])

			response, err := client.Do(request1)
			assert.Nil(t, err)
			assert.Equal(t, http.StatusOK, response.StatusCode)

			request2, err := http.NewRequest("POST", testServer.URL, nil)
			assert.Nil(t, err)
			request2.Header.Add("CustomerID", tt.customerIDs[1])

			response, err = client.Do(request2)
			assert.Nil(t, err)
			assert.Equal(t, tt.secondRequestStatus, response.StatusCode)
		})
	}
}
@rvdwijngaard
Copy link

I was actually thinking the same; @didip can you please give your comments on this issue?

@jack-chung
Copy link

Agreed the headerValues part of the code looks not right. It's not checking the configured values against the request value, and also not using the request value to build the key.

@didip
Copy link
Owner

didip commented Jul 10, 2019

Man, I keep not getting notified over email.

This does look like a bug. I will take a look at it closer, it has been a while.

@didip didip closed this as completed in be0cf69 Jul 15, 2019
@didip
Copy link
Owner

didip commented Jul 15, 2019

This is a legitimate bug, see fix SHA above.

@jack-chung
Copy link

@didip For the case of a configured header name with no value, the new code puts the header name into the key but no value. I thought it's supposed to include the header value in the request as part of the key? Or did I misunderstand the intended use case?

Example:
If I want to throttle on a header called X-USER-ID, so that each user is bound by a request limit, this is not going to work with the new code (nor the old code). Only the header name is in the key, so different users coming from the same IP (e.g. behind corporate firewall) would all be subject to the same aggregated limit, not individual limits.

@didip
Copy link
Owner

didip commented Jul 23, 2019

@jack-chung If you only define request header (without values) and if everyone visiting has X-USER-ID, then yes, they all limited under the same bucket.

Do you think they should all be limited as individual limit?

didip added a commit that referenced this issue Jul 23, 2019
…oth,

Create individual bucket by using request header’s own value,

so that we can rate-limit individually.

Also fix a regression where we didn’t pass BasicAuth username on one of the possible path of key building.
@didip
Copy link
Owner

didip commented Jul 23, 2019

v4.0.2

@jack-chung
Copy link

@didip Yes that's how I see this feature should work, based on the use case I outlined above. Also, this new code still supports the "aggregated limit" use case if needed. Just set the header value to a static value for all users, then it works just like before.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

No branches or pull requests

4 participants