httprate

package module
v0.14.1 Latest Latest
Warning

This package is not in the latest version of its module.

Go to latest
Published: Aug 28, 2024 License: MIT Imports: 9 Imported by: 171

README

httprate - HTTP Rate Limiter

CI workflow Benchmark workflow GoDoc Widget

net/http request rate limiter based on the Sliding Window Counter pattern inspired by CloudFlare https://blog.cloudflare.com/counting-things-a-lot-of-different-things.

The sliding window counter pattern is accurate, smooths traffic and offers a simple counter design to share a rate-limit among a cluster of servers. For example, if you'd like to use redis to coordinate a rate-limit across a group of microservices you just need to implement the httprate.LimitCounter interface to support an atomic increment and get.

Backends

Example

package main

import (
	"net/http"

	"github.com/go-chi/chi/v5"
	"github.com/go-chi/chi/v5/middleware"
	"github.com/go-chi/httprate"
)

func main() {
	r := chi.NewRouter()
	r.Use(middleware.Logger)

	// Enable httprate request limiter of 100 requests per minute.
	//
	// In the code example below, rate-limiting is bound to the request IP address
	// via the LimitByIP middleware handler.
	//
	// To have a single rate-limiter for all requests, use httprate.LimitAll(..).
	//
	// Please see _example/main.go for other more, or read the library code.
	r.Use(httprate.LimitByIP(100, time.Minute))

	r.Get("/", func(w http.ResponseWriter, r *http.Request) {
		w.Write([]byte("."))
	})

	http.ListenAndServe(":3333", r)
}

Common use cases

Rate limit by IP and URL path (aka endpoint)
r.Use(httprate.Limit(
	10,             // requests
	10*time.Second, // per duration
	httprate.WithKeyFuncs(httprate.KeyByIP, httprate.KeyByEndpoint),
))
Rate limit by arbitrary keys
r.Use(httprate.Limit(
	100,
	time.Minute,
	// an oversimplified example of rate limiting by a custom header
	httprate.WithKeyFuncs(func(r *http.Request) (string, error) {
		return r.Header.Get("X-Access-Token"), nil
	}),
))
Rate limit by request payload
// Rate-limiter for login endpoint.
loginRateLimiter := httprate.NewRateLimiter(5, time.Minute)

r.Post("/login", func(w http.ResponseWriter, r *http.Request) {
	var payload struct {
		Username string `json:"username"`
		Password string `json:"password"`
	}
	err := json.NewDecoder(r.Body).Decode(&payload)
	if err != nil || payload.Username == "" || payload.Password == "" {
		w.WriteHeader(400)
		return
	}

	// Rate-limit login at 5 req/min.
	if loginRateLimiter.RespondOnLimit(w, r, payload.Username) {
		return
	}

	w.Write([]byte("login at 5 req/min\n"))
})
Send specific response for rate-limited requests

The default response is HTTP 429 with Too Many Requests body. You can override it with:

r.Use(httprate.Limit(
	10,
	time.Minute,
	httprate.WithLimitHandler(func(w http.ResponseWriter, r *http.Request) {
		http.Error(w, `{"error": "Rate-limited. Please, slow down."}`, http.StatusTooManyRequests)
	}),
))
Send specific response on errors

An error can be returned by:

  • A custom key function provided by httprate.WithKeyFunc(customKeyFn)
  • A custom backend provided by httprateredis.WithRedisLimitCounter(customBackend)
    • The default local in-memory counter is guaranteed not return any errors
    • Backends that fall-back to the local in-memory counter (e.g. httprate-redis) can choose not to return any errors either
r.Use(httprate.Limit(
	10,
	time.Minute,
	httprate.WithErrorHandler(func(w http.ResponseWriter, r *http.Request, err error) {
		http.Error(w, fmt.Sprintf(`{"error": %q}`, err), http.StatusPreconditionRequired)
	}),
	httprate.WithLimitCounter(customBackend),
))
Send custom response headers
r.Use(httprate.Limit(
	1000,
	time.Minute,
	httprate.WithResponseHeaders(httprate.ResponseHeaders{
		Limit:      "X-RateLimit-Limit",
		Remaining:  "X-RateLimit-Remaining",
		Reset:      "X-RateLimit-Reset",
		RetryAfter: "Retry-After",
		Increment:  "", // omit
	}),
))
Omit response headers
r.Use(httprate.Limit(
	1000,
	time.Minute,
	httprate.WithResponseHeaders(httprate.ResponseHeaders{}),
))

LICENSE

MIT

Documentation

Index

Constants

This section is empty.

Variables

This section is empty.

Functions

func Key added in v0.14.1

func Key(key string) func(r *http.Request) (string, error)

func KeyByEndpoint

func KeyByEndpoint(r *http.Request) (string, error)

func KeyByIP

func KeyByIP(r *http.Request) (string, error)

func KeyByRealIP added in v0.6.0

func KeyByRealIP(r *http.Request) (string, error)

func Limit

func Limit(requestLimit int, windowLength time.Duration, options ...Option) func(next http.Handler) http.Handler

func LimitAll

func LimitAll(requestLimit int, windowLength time.Duration) func(next http.Handler) http.Handler

func LimitByIP

func LimitByIP(requestLimit int, windowLength time.Duration) func(next http.Handler) http.Handler

func LimitByRealIP added in v0.6.0

func LimitByRealIP(requestLimit int, windowLength time.Duration) func(next http.Handler) http.Handler

func LimitCounterKey

func LimitCounterKey(key string, window time.Time) uint64

func NewLocalLimitCounter added in v0.12.0

func NewLocalLimitCounter(windowLength time.Duration) *localCounter

NewLocalLimitCounter creates an instance of localCounter, which is an in-memory implementation of http.LimitCounter.

All methods are guaranteed to always return nil error.

func WithIncrement added in v0.8.0

func WithIncrement(ctx context.Context, value int) context.Context

func WithRequestLimit added in v0.9.0

func WithRequestLimit(ctx context.Context, value int) context.Context

Types

type KeyFunc

type KeyFunc func(r *http.Request) (string, error)

type LimitCounter

type LimitCounter interface {
	Config(requestLimit int, windowLength time.Duration)
	Increment(key string, currentWindow time.Time) error
	IncrementBy(key string, currentWindow time.Time, amount int) error
	Get(key string, currentWindow, previousWindow time.Time) (int, int, error)
}

type Option

type Option func(rl *RateLimiter)

func WithErrorHandler added in v0.12.1

func WithErrorHandler(h func(http.ResponseWriter, *http.Request, error)) Option

func WithKeyByIP added in v0.7.0

func WithKeyByIP() Option

func WithKeyByRealIP added in v0.7.0

func WithKeyByRealIP() Option

func WithKeyFuncs

func WithKeyFuncs(keyFuncs ...KeyFunc) Option

func WithLimitCounter

func WithLimitCounter(c LimitCounter) Option

func WithLimitHandler

func WithLimitHandler(h http.HandlerFunc) Option

func WithNoop added in v0.7.3

func WithNoop() Option

func WithResponseHeaders added in v0.11.0

func WithResponseHeaders(headers ResponseHeaders) Option

type RateLimiter added in v0.13.1

type RateLimiter struct {
	// contains filtered or unexported fields
}

func NewRateLimiter

func NewRateLimiter(requestLimit int, windowLength time.Duration, options ...Option) *RateLimiter

func (*RateLimiter) Counter added in v0.13.1

func (l *RateLimiter) Counter() LimitCounter

func (*RateLimiter) Handler added in v0.13.1

func (l *RateLimiter) Handler(next http.Handler) http.Handler

func (*RateLimiter) OnLimit added in v0.13.1

func (l *RateLimiter) OnLimit(w http.ResponseWriter, r *http.Request, key string) bool

OnLimit checks the rate limit for the given key and updates the response headers accordingly. If the limit is reached, it returns true, indicating that the request should be halted. Otherwise, it increments the request count and returns false. This method does not send an HTTP response, so the caller must handle the response themselves or use the RespondOnLimit() method instead.

func (*RateLimiter) RespondOnLimit added in v0.14.0

func (l *RateLimiter) RespondOnLimit(w http.ResponseWriter, r *http.Request, key string) bool

RespondOnLimit checks the rate limit for the given key and updates the response headers accordingly. If the limit is reached, it automatically sends an HTTP response and returns true, signaling the caller to halt further request processing. If the limit is not reached, it increments the request count and returns false, allowing the request to proceed.

func (*RateLimiter) Status added in v0.13.1

func (l *RateLimiter) Status(key string) (bool, float64, error)

type ResponseHeaders added in v0.11.0

type ResponseHeaders struct {
	Limit      string // Default: X-RateLimit-Limit
	Remaining  string // Default: X-RateLimit-Remaining
	Increment  string // Default: X-RateLimit-Increment
	Reset      string // Default: X-RateLimit-Reset
	RetryAfter string // Default: Retry-After
}

Set custom response headers. If empty, the header is omitted.

Jump to

Keyboard shortcuts

? : This menu
/ : Search site
f or F : Jump to
y or Y : Canonical URL