Naming, layout and comments: descriptive full-word names, one item per line in wrapped calls and literals, and doc comments on exported names

Short and single-letter names in application code become full words (tests
keep theirs). Wrapped calls, signatures and composite literals put one item
per line, and one-line function bodies are expanded. Exported names and
groups of constants get doc comments, and comments refer to the renamed
identifiers.
This commit is contained in:
bwilliams committed 2026-10-15 16:12:56 +01:00
1 parent 043e307663
commit 465358609a
78 files changed
+2374 -1402

No files matched your search

+55 -40
View File
@@ -24,6 +24,7 @@ type Limiter interface {
// LimiterFunc adapts a function to the Limiter interface.
type LimiterFunc func(key string) (bool, time.Duration)
// Allow implements Limiter by calling f.
func (f LimiterFunc) Allow(key string) (bool, time.Duration) { return f(key) }
// KeyFunc derives the rate limit key from a request.
@@ -34,22 +35,24 @@ func KeyByIP(r *http.Request) string { return ClientIP(r) }
// RateLimit turns a Limiter into middleware. Rejected requests get 429 with
// a Retry-After header. A nil limiter yields a pass-through middleware.
func RateLimit(l Limiter, key KeyFunc) func(http.Handler) http.Handler {
if l == nil {
return func(next http.Handler) http.Handler { return next }
func RateLimit(limiter Limiter, key KeyFunc) func(http.Handler) http.Handler {
if limiter == nil {
return func(next http.Handler) http.Handler {
return next
}
}
if key == nil {
key = KeyByIP
}
return func(next http.Handler) http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
ok, retry := l.Allow(key(r))
ok, retry := limiter.Allow(key(r))
if !ok {
secs := int(math.Ceil(retry.Seconds()))
if secs < 1 {
secs = 1
seconds := int(math.Ceil(retry.Seconds()))
if seconds < 1 {
seconds = 1
}
w.Header().Set("Retry-After", strconv.Itoa(secs))
w.Header().Set("Retry-After", strconv.Itoa(seconds))
httpx.WriteProblem(w, r, http.StatusTooManyRequests, "rate limit exceeded")
return
}
@@ -74,22 +77,32 @@ type limiterShard struct {
}
type limiterEntry struct {
lim *rate.Limiter
last time.Time
limiter *rate.Limiter
last time.Time
}
// NewKeyedLimiter creates a limiter allowing rps sustained requests per
// second with the given burst. A janitor goroutine evicts entries idle for
// longer than ttl until ctx is cancelled.
func NewKeyedLimiter(ctx context.Context, rps float64, burst int, ttl time.Duration) *KeyedLimiter {
k := &KeyedLimiter{limit: rate.Limit(rps), burst: burst, ttl: ttl, seed: maphash.MakeSeed()}
for i := range k.shards {
k.shards[i].entries = make(map[string]*limiterEntry)
// NewKeyedLimiter creates a limiter allowing requestsPerSecond sustained
// requests per second with the given burst. A janitor goroutine evicts
// entries idle for longer than ttl until ctx is cancelled.
func NewKeyedLimiter(
ctx context.Context,
requestsPerSecond float64,
burst int,
ttl time.Duration,
) *KeyedLimiter {
limiter := &KeyedLimiter{
limit: rate.Limit(requestsPerSecond),
burst: burst,
ttl: ttl,
seed: maphash.MakeSeed(),
}
for i := range limiter.shards {
limiter.shards[i].entries = make(map[string]*limiterEntry)
}
if ttl > 0 {
go k.janitor(ctx)
go limiter.janitor(ctx)
}
return k
return limiter
}
func (k *KeyedLimiter) shard(key string) *limiterShard {
@@ -99,45 +112,47 @@ func (k *KeyedLimiter) shard(key string) *limiterShard {
// Allow implements Limiter.
func (k *KeyedLimiter) Allow(key string) (bool, time.Duration) {
now := time.Now()
s := k.shard(key)
s.mu.Lock()
e := s.entries[key]
if e == nil {
e = &limiterEntry{lim: rate.NewLimiter(k.limit, k.burst)}
s.entries[key] = e
shard := k.shard(key)
shard.mu.Lock()
entry := shard.entries[key]
if entry == nil {
entry = &limiterEntry{
limiter: rate.NewLimiter(k.limit, k.burst),
}
shard.entries[key] = entry
}
e.last = now
lim := e.lim
s.mu.Unlock()
entry.last = now
limiter := entry.limiter
shard.mu.Unlock()
res := lim.ReserveN(now, 1)
res := limiter.ReserveN(now, 1)
if !res.OK() {
return false, time.Second
}
if d := res.DelayFrom(now); d > 0 {
if delay := res.DelayFrom(now); delay > 0 {
res.CancelAt(now)
return false, d
return false, delay
}
return true, 0
}
func (k *KeyedLimiter) janitor(ctx context.Context) {
t := time.NewTicker(k.ttl / 2)
defer t.Stop()
ticker := time.NewTicker(k.ttl / 2)
defer ticker.Stop()
for {
select {
case <-ctx.Done():
return
case now := <-t.C:
case now := <-ticker.C:
for i := range k.shards {
s := &k.shards[i]
s.mu.Lock()
for key, e := range s.entries {
if now.Sub(e.last) > k.ttl {
delete(s.entries, key)
shard := &k.shards[i]
shard.mu.Lock()
for key, entry := range shard.entries {
if now.Sub(entry.last) > k.ttl {
delete(shard.entries, key)
}
}
s.mu.Unlock()
shard.mu.Unlock()
}
}
}
+2 -1
View File
@@ -8,7 +8,8 @@ import (
func TestRateLimit(t *testing.T) {
l := NewKeyedLimiter(t.Context(), 1, 2, 0)
h := RateLimit(l, nil)(http.HandlerFunc(func(http.ResponseWriter, *http.Request) {}))
h := RateLimit(l, nil)(http.HandlerFunc(func(http.ResponseWriter, *http.Request) {
}))
var last int
for i := 0; i < 4; i++ {
rec := httptest.NewRecorder()
+45 -41
View File
@@ -20,8 +20,8 @@ type ClientInfo struct {
// Client returns the ClientInfo stored by RealIP. Without RealIP it falls back
// to the connection itself (see peerInfo) and ignores forwarding headers.
func Client(r *http.Request) ClientInfo {
if ci, ok := r.Context().Value(clientInfoKey).(ClientInfo); ok {
return ci
if info, ok := r.Context().Value(clientInfoKey).(ClientInfo); ok {
return info
}
return peerInfo(r)
}
@@ -62,10 +62,14 @@ func peerInfo(r *http.Request) ClientInfo {
// RealIPOptions configures RealIP.
type RealIPOptions struct {
// Trusted are the proxies whose forwarding headers are believed.
Trusted []netip.Prefix
// Headers is processed in increasing priority order - the last header in the
// list that is found is the header used to determine a client IP address.
Headers []string // e.g. X-Forwarded-For, Forwarded, X-Real-IP
// Headers are the headers the client IP is read from, in priority order:
// the first of them that the request has and that gives an address is
// used. E.g. X-Forwarded-For, Forwarded, X-Real-IP.
Headers []string
// StripUntrusted removes the forwarding headers from requests that are
// not from a trusted proxy.
StripUntrusted bool
}
@@ -91,9 +95,9 @@ var forwardingHeaders = []string{
// PROXY protocol is handled at the listener (see server package), so by the
// time this runs r.RemoteAddr already holds the address from the PROXY header.
func RealIP(opts RealIPOptions) func(http.Handler) http.Handler {
trusted := func(a netip.Addr) bool {
for _, p := range opts.Trusted {
if p.Contains(a) {
trusted := func(addr netip.Addr) bool {
for _, prefix := range opts.Trusted {
if prefix.Contains(addr) {
return true
}
}
@@ -105,19 +109,19 @@ func RealIP(opts RealIPOptions) func(http.Handler) http.Handler {
info := peerInfo(r)
if info.Peer.IsValid() && trusted(info.Peer) {
for _, h := range opts.Headers {
if ip, ok := clientFromHeader(r.Header, h, trusted); ok {
for _, name := range opts.Headers {
if ip, ok := clientFromHeader(r.Header, name, trusted); ok {
info.IP = ip
info.ViaProxy = true
break
}
}
if p := forwardedProto(r.Header); p == "http" || p == "https" {
info.Scheme = p
if proto := forwardedProto(r.Header); proto == "http" || proto == "https" {
info.Scheme = proto
}
} else if opts.StripUntrusted {
for _, h := range forwardingHeaders {
r.Header.Del(h)
for _, name := range forwardingHeaders {
r.Header.Del(name)
}
}
@@ -128,17 +132,17 @@ func RealIP(opts RealIPOptions) func(http.Handler) http.Handler {
}
func clientFromHeader(
h http.Header, name string, trusted func(netip.Addr) bool,
header http.Header, name string, trusted func(netip.Addr) bool,
) (netip.Addr, bool) {
switch http.CanonicalHeaderKey(name) {
case "X-Forwarded-For":
return walkChain(splitList(h.Values("X-Forwarded-For")), trusted)
return walkChain(splitList(header.Values("X-Forwarded-For")), trusted)
case "Forwarded":
var chain []string
for _, el := range splitList(h.Values("Forwarded")) {
if v, ok := forwardedParam(el, "for"); ok {
chain = append(chain, v)
for _, element := range splitList(header.Values("Forwarded")) {
if value, ok := forwardedParam(element, "for"); ok {
chain = append(chain, value)
} else {
chain = append(chain, "") // element without for= breaks the chain
}
@@ -146,8 +150,8 @@ func clientFromHeader(
return walkChain(chain, trusted)
case "X-Real-Ip":
if v := strings.TrimSpace(h.Get("X-Real-IP")); v != "" {
return parseIPToken(v)
if value := strings.TrimSpace(header.Get("X-Real-IP")); value != "" {
return parseIPToken(value)
}
}
@@ -174,15 +178,15 @@ func walkChain(
return last, last.IsValid()
}
func forwardedProto(h http.Header) string {
if v := h.Get("X-Forwarded-Proto"); v != "" {
first, _, _ := strings.Cut(v, ",")
func forwardedProto(header http.Header) string {
if value := header.Get("X-Forwarded-Proto"); value != "" {
first, _, _ := strings.Cut(value, ",")
return strings.ToLower(strings.TrimSpace(first))
}
if els := splitList(h.Values("Forwarded")); len(els) > 0 {
if v, ok := forwardedParam(els[0], "proto"); ok {
return strings.ToLower(v)
if elements := splitList(header.Values("Forwarded")); len(elements) > 0 {
if value, ok := forwardedParam(elements[0], "proto"); ok {
return strings.ToLower(value)
}
}
@@ -193,9 +197,9 @@ func forwardedProto(h http.Header) string {
// element (RFC 7239).
func forwardedParam(element, key string) (string, bool) {
for _, pair := range strings.Split(element, ";") {
k, v, ok := strings.Cut(strings.TrimSpace(pair), "=")
if ok && strings.EqualFold(k, key) {
return strings.Trim(strings.TrimSpace(v), `"`), true
name, value, ok := strings.Cut(strings.TrimSpace(pair), "=")
if ok && strings.EqualFold(name, key) {
return strings.Trim(strings.TrimSpace(value), `"`), true
}
}
return "", false
@@ -203,8 +207,8 @@ func forwardedParam(element, key string) (string, bool) {
func splitList(values []string) []string {
var out []string
for _, v := range values {
for _, part := range strings.Split(v, ",") {
for _, value := range values {
for _, part := range strings.Split(value, ",") {
if part = strings.TrimSpace(part); part != "" {
out = append(out, part)
}
@@ -214,23 +218,23 @@ func splitList(values []string) []string {
}
// parseIPToken accepts "1.2.3.4", "1.2.3.4:80", "::1", "[::1]" and "[::1]:80".
func parseIPToken(s string) (netip.Addr, bool) {
s = strings.Trim(strings.TrimSpace(s), `"`)
if ap, err := netip.ParseAddrPort(s); err == nil {
return ap.Addr().Unmap(), true
func parseIPToken(token string) (netip.Addr, bool) {
token = strings.Trim(strings.TrimSpace(token), `"`)
if addrPort, err := netip.ParseAddrPort(token); err == nil {
return addrPort.Addr().Unmap(), true
}
s = strings.TrimSuffix(strings.TrimPrefix(s, "["), "]")
if a, err := netip.ParseAddr(s); err == nil {
return a.Unmap().WithZone(""), true
token = strings.TrimSuffix(strings.TrimPrefix(token, "["), "]")
if addr, err := netip.ParseAddr(token); err == nil {
return addr.Unmap().WithZone(""), true
}
return netip.Addr{}, false
}
func addrFromRemote(remote string) netip.Addr {
if a, ok := parseIPToken(remote); ok {
return a
if addr, ok := parseIPToken(remote); ok {
return addr
}
return netip.Addr{}
}
+3 -1
View File
@@ -12,7 +12,9 @@ import (
func serveRealIP(t *testing.T, opts RealIPOptions, peer string, h map[string]string) ClientInfo {
t.Helper()
var got ClientInfo
next := http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { got = Client(r) })
next := http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
got = Client(r)
})
req := httptest.NewRequest("GET", "/", nil)
req.RemoteAddr = peer
for k, v := range h {