Search code examples
gorate-limitinggo-http

Rate limit specific endpoints


I am new to GoLang and working on my first API. I have two endpoints, and I want to rate limit only one of them. I found a helpful tutorial to get me started, and I've based my approach off of the tutorial, recognizing that this approach will rate limit both of my endpoints:

var limiter = rate.NewLimiter(rate.Every((1*time.Hour)/3), 1)

func limit(next http.Handler) http.Handler {
    return http.HandlerFunc(func(res http.ResponseWriter, req *http.Request) {
        if limiter.Allow() == false {
            http.Error(res, http.StatusText(429), http.StatusTooManyRequests)
            return
        }
        next.ServeHTTP(res, req)
    })
}

func main() {
    mux := http.NewServeMux()
    mux.HandleFunc("/", createNewToken)
    mux.HandleFunc("/notify", sendPushNotificationToAllTokens)

    log.Fatal(http.ListenAndServeTLS(":5050", "localhost.crt", "localhost.key", limit(mux)))
}

I researched the difference between http.Handle and http.HandleFunc and naively believed that I could substitute http.HandleFunc for http.Handle. This approach is completely flawed as the logic contained in the HandlerFunc never executes:

var limiter = rate.NewLimiter(rate.Every(1*time.Hour/3), 1)

func limit(next http.HandlerFunc) http.HandlerFunc {
    return func(res http.ResponseWriter, req *http.Request) {
        if limiter.Allow() == false {
            http.Error(res, http.StatusText(429), http.StatusTooManyRequests)
            return
        }
        next.ServeHTTP(res, req)
    }
}

func main() {
    //mux := http.NewServeMux()
    http.HandleFunc("/", createNewToken)
    http.HandleFunc("/notify", sendPushNotificationToAllTokens)

    // attempt to only rate limit the /notify endpoint 
    log.Fatal(http.ListenAndServeTLS(":5050", "localhost.crt", "localhost.key", limit(sendPushNotificationToAllTokens)))

Can anyone explain why this does not work, and how I could approach this problem to only rate limit a specific endpoint?


Solution

  • The distinction between using a plain http.Handler and a http.HanlderFunc doesn't really matter here. http.HandleFunc is just a way to convert a regular function into a http.Handler - it essentially does the same thing as your original version of limit.

    Your implementations of limit both look fine; probably the second is better because it's simpler. Instead, the issue is in main. When you call http.ListenAndServeTLS and provide a value for the final argument, it requests that only the handler you pass in as that final argument be used as the root request handler. Any calls to http.Handle() or http.HandleFunc() are ignored unless you pass in nil as this final argument.

    What you want to do instead is apply limit to the specific handler you want to limit. You have two options for this. First, you can use a ServeMux like in your first code snippet:

    func main() {
        mux := http.NewServeMux()
        mux.HandleFunc("/", createNewToken)
        // Limit only the handler for "/notify".
        mux.HandleFunc("/notify", limit(sendPushNotificationToAllTokens))
    
        // Don't limit the whole mux.
        log.Fatal(http.ListenAndServeTLS(":5050", "localhost.crt", "localhost.key", mux))
    }
    

    Alternatively, you can do something more like your second code snippet, but pass in nil for the final argument to http.ListenAndServeTLS so that the default http.ServeMux is used, meaning that the calls to http.HandleFunc() will be respected:

    func main() {
        http.HandleFunc("/", createNewToken)
        // Limit only the handler for "/notify".
        http.HandleFunc("/notify", limit(sendPushNotificationToAllTokens))
    
        // Pass in nil here so that http.DefaultServeMux is used.
        log.Fatal(http.ListenAndServeTLS(":5050", "localhost.crt", "localhost.key", nil))
    }
    

    For a simple application, the first approach is probably fine. For anything more complex, I'd recommend the later approach because it will work if you open multiple servers or do other more complex things.