Go · 2401 bytes Raw Blame History
1 // SPDX-License-Identifier: AGPL-3.0-or-later
2
3 package ratelimit
4
5 import (
6 "net/http"
7 "strconv"
8 "time"
9 )
10
11 // KeyFunc derives the per-request rate-limit key. Common choices:
12 // remote IP (anon), user id (authed), token id (PAT), repo id+user
13 // (per-repo), etc. Returning "" skips the limiter (caller should
14 // only return "" when there's nothing meaningful to throttle on).
15 type KeyFunc func(*http.Request) string
16
17 // Middleware wraps next with a per-request Allow check. On allow,
18 // X-RateLimit-* headers are stamped on the response; on deny, the
19 // response is 429 with Retry-After.
20 //
21 // `name` is the per-route identifier surfaced in logs and lets the
22 // admin observability pages disambiguate scopes that share the same
23 // counter (e.g. "api:anon" used by both /api/repos and /api/users).
24 func (l *Limiter) Middleware(p Policy, key KeyFunc) func(http.Handler) http.Handler {
25 return func(next http.Handler) http.Handler {
26 return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
27 k := ""
28 if key != nil {
29 k = key(r)
30 }
31 if k == "" {
32 next.ServeHTTP(w, r)
33 return
34 }
35 d, err := l.Allow(r.Context(), p, k)
36 if err != nil {
37 // Fail-open on counter errors — surface in logs via
38 // the wrapped handler's logging middleware. We still
39 // stamp the headers we know.
40 StampHeaders(w, d)
41 next.ServeHTTP(w, r)
42 return
43 }
44 StampHeaders(w, d)
45 if !d.Allowed {
46 w.Header().Set("Retry-After", strconv.Itoa(int(d.RetryAfter/time.Second)))
47 // Headers MUST be set before WriteHeader for the
48 // response Content-Type to land; matches the same
49 // pattern as the writeRetryAfter fix from S05.
50 w.Header().Set("Content-Type", "text/plain; charset=utf-8")
51 w.WriteHeader(http.StatusTooManyRequests)
52 _, _ = w.Write([]byte("Rate limit exceeded. Please retry after the X-RateLimit-Reset window.\n"))
53 return
54 }
55 next.ServeHTTP(w, r)
56 })
57 }
58 }
59
60 // StampHeaders writes the standard X-RateLimit-* headers from a
61 // Decision. Exposed so handlers that run a manual Allow can apply
62 // the same stamp without going through Middleware.
63 func StampHeaders(w http.ResponseWriter, d Decision) {
64 if d.Limit > 0 {
65 w.Header().Set("X-RateLimit-Limit", strconv.Itoa(d.Limit))
66 w.Header().Set("X-RateLimit-Remaining", strconv.Itoa(d.Remaining))
67 w.Header().Set("X-RateLimit-Reset", strconv.Itoa(int(d.ResetIn/time.Second)))
68 }
69 }
70