Rate limiter gorilla mux

Hi,

I am trying to implement http request limiter to allow 10 request per second.
Below is what I have implemented with reference of rate-limit.

// Run a background goroutine to remove old entries from the visitors map.
func init() {
	fmt.Println("This will get called on main initialization")
	go cleanupVisitors()
}

func getVisitor(username string) *rate.Limiter {
	mu.Lock()
	defer mu.Unlock()
	v, exists := visitors[username]
	if !exists {
		//rt := rate.Every(1 * time.Minute)
		//limiter := rate.NewLimiter(rt, 1)
		limiter := rate.NewLimiter(5, 3)
		// Include the current time when creating a new visitor.
		visitors[username] = &visitor{limiter, time.Now()}
		return limiter
	}

	// Update the last seen time for the visitor.
	v.lastSeen = time.Now()
	return v.limiter
}

// Every minute check the map for visitors that haven't been seen for
// more than 3 minutes and delete the entries.
func cleanupVisitors() {
	for {
		time.Sleep(time.Minute)
		mu.Lock()
		for username, v := range visitors {
			if time.Since(v.lastSeen) > 1*time.Minute {
				delete(visitors, username)
			}
		}
		mu.Unlock()
	}
}

func limit(next http.Handler) http.Handler {
	return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
		mappedArray := hotelapi.SearchResponse{}
		mappedArray.StartTime = time.Now().Format("2006-02-01 15:04:05.000000")
		mappedArray.EndTime = time.Now().Format("2006-02-01 15:04:05.000000")
		userName := r.FormValue("username")
		limiter := getVisitor(userName)
		if !limiter.Allow() {
			w.Header().Set("Content-Type", "application/json")
			w.WriteHeader(http.StatusTooManyRequests)
			mappedArray.MessageInfo = http.StatusText(http.StatusTooManyRequests)
			mappedArray.ErrorCode = strconv.Itoa(http.StatusTooManyRequests)
			json.NewEncoder(w).Encode(mappedArray)
			return
		}

		next.ServeHTTP(w, r)
	})
}

func route() {
	r := mux.NewRouter()
	r.PathPrefix("/hello").HandlerFunc(api.ProcessSupplier).Methods("GET")
	ws := r.PathPrefix("/index.php").HandlerFunc(api.ProcessWebService).Methods("GET", "POST").Subrouter()
	r.Use(panicRecovery)
	ws.Use(limit)
	http.HandleFunc("/favicon.ico", faviconHandler)
	if config.HTTPSEnabled {
		err := http.ListenAndServeTLS(":"+config.Port, config.HTTPSCertificateFilePath, config.HTTPSKeyFilePath, handlers.CompressHandlerLevel(r, gzip.BestSpeed))
		if err != nil {
			fmt.Println(err)
			log.Println(err)
		}
	} else {
		err := http.ListenAndServe(":"+config.Port, limit(handlers.CompressHandler(r)))
		if err != nil {
			fmt.Println(err)
			log.Println(err)
		}
	}
}

I have couple of concerns here.

  1. I want limiter only for /index.php and not for /hello. I did implement with Subroute. Is it correct way?
  2. The limit middleware is not limiting as I assumed. It allows 1 successful request all other requests are returned with too many requests error.

What am I missing here. ?

Better use idiomatic middleware. You don’t need to use chi, you can use it just with http package or Gorilla mux.

1 Like

Hi @acim,
Thank you for reply. I did try this.

ws.Use(httprate.Limit(
		10,            // requests
		1*time.Second, // per duration
		httprate.WithKeyFuncs(func(r *http.Request) (string, error) {
			return r.FormValue("username"), nil
		}),
		httprate.WithLimitHandler(func(w http.ResponseWriter, r *http.Request) {
			http.Error(w, http.StatusText(http.StatusTooManyRequests), http.StatusTooManyRequests)
		}),
	))

Then with jmeter I did 12 requests per second. All requests getting an error too many requests

Do you mean it returns too many requests even within the first 10? If so, please give us the full setup of the router. You may also try just simple one:

ws.Use(httprate.LimitByIP(3, 5*time.Second))

Instead of the one above. And for the one above, try to set 100 requests per 10 seconds, it maybe makes some difference.

1 Like

Yes. It returns too many requests within first 10 requests.
Let me try with LimitByIP.
Also I will share setup of router.

Hello, Thanks . I did changes and its working . I am making more tests.
Just one concern, how can I make limit and duration dynamic for each user ?
I am trying to access username with r.FormValue("username") but since httprate.Limit doesn’t have r *http.Request I am getting blank username.

Well, you have to use the first method that didn’t work well and debug what’s wrong or eventually report an issue to go-chi/httprate. Also check the examples and tests there.

Sorry. Forgot to mention. I tried with LimitByIP and accordingly debug and corrected the code which I was trying in first method. Now first method also working as expected. Now I am just struggling to make limit dynamic based on username field in URL. I checked the examples but couldn’t find any. Trying more. If not found will raise with go-chi/httprate.
Thank you.

https://raw.githubusercontent.com/go-chi/httprate/master/_example/main.go

		r.Use(httprate.Limit(
			10,
			10*time.Second,
			httprate.WithKeyFuncs(httprate.KeyByIP, func(r *http.Request) (string, error) {
				token := r.Context().Value("userID").(string)
				return token, nil
			}),
			httprate.WithLimitHandler(func(w http.ResponseWriter, r *http.Request) {
				// We can send custom responses for the rate limited requests, e.g. a JSON message
				w.Header().Set("Content-Type", "application/json")
				w.WriteHeader(http.StatusTooManyRequests)
				w.Write([]byte(`{"error": "Too many requests"}`))
			}),
		))

You have to modify httprate.WithKeyFuncs to use your way of finding the userID (from header, from cookie, from query or whatever else).

1 Like

I did this. For testing purpose I passed token as a random text (not actual username) and still strangely limiter was applied on it.
My actual username is : ganesh in r.FormValue(“username”).

ws.Use(httprate.Limit(
		5,
		1*time.Second,
		httprate.WithKeyFuncs(func(r *http.Request) (string, error) {
			return "test", nil
		}),
		httprate.WithLimitHandler(func(w http.ResponseWriter, r *http.Request) {
			w.Header().Set("Content-Type", "application/json")
				w.WriteHeader(http.StatusTooManyRequests)
				w.Write([]byte(`{"error": "Too many requests"}`))
		}),
	))
HTTP/1.1 429 Too Many Requests
Content-Type: application/json
Retry-After: 1
X-Ratelimit-Limit: 5
X-Ratelimit-Remaining: 0
X-Ratelimit-Reset: 1634128918
Date: Wed, 13 Oct 2021 12:41:57 GMT
Content-Length: 271

But where did you apply the logic to skip the limiter for some users? I don’t see that. You should do that in WithKeyFuncs.