Documentation
¶
Overview ¶
Package ratelimit is the in-binary request limiter (ADR 0025). Flood limits are in-memory sliding windows (go-chi/httprate); a limit of 0 disables its policy.
Seams for the other slices:
- NewPolicy builds one named limiter; Policy.Allow checks a key and, on a rejection, answers the 429 itself (headers, problem body, metric, log);
- Rejected is the policy-named metric + warn log for callers that render the 429 their own way (e.g. a typed error from an ogen security handler);
- WriteProblem renders the 429 body;
- Exchange is the request's writer and request, carried in the context by Middleware, for code that only receives a ctx (the ogen security handlers): it charges the Workspace budget and the failed-authentication limit and reports a *LimitedError that the error handler renders with WriteProblem.
Index ¶
- Constants
- func Rejected(ctx context.Context, policy string)
- func WriteProblem(w http.ResponseWriter)
- type Exchange
- func (e *Exchange) AuthBlocked(ctx context.Context) error
- func (e *Exchange) AuthFailed(ctx context.Context) error
- func (e *Exchange) ChargeCollect(workspaceID int64) error
- func (e *Exchange) ChargeService(credentialID int64) error
- func (e *Exchange) ChargeWorkspace(workspaceID int64) error
- func (e *Exchange) Delay(ctx context.Context, policy string, limit int, wait time.Duration, ...) error
- type LimitedError
- type Limiter
- type Policy
- func (p *Policy) Allow(w http.ResponseWriter, r *http.Request, key string) bool
- func (p *Policy) Blocked(w http.ResponseWriter, r *http.Request, key string) error
- func (p *Policy) Exceeded(r *http.Request, key string) bool
- func (p *Policy) Reject(w http.ResponseWriter, r *http.Request, key string) error
- type Refusal
Constants ¶
const ( PolicyHuman = "human" // PolicyTestSend is the cap on Test sends from the System sender (ADR 0041). PolicyTestSend = "test-send" PolicyAPIBurst = "api-burst" PolicyAPI = "api" PolicyFailedAuth = "failed-auth" // PolicyTracking is the recording guard of opens and clicks. It never refuses. PolicyTracking = "tracking" // PolicyLoginAccount is the per-account login delay, PolicyLoginIP the per-IP cap // on login requests. PolicyLoginAccount = "login-account" PolicyLoginIP = "login-ip" // PolicyCollect is the /collect budget per Workspace, PolicyCollectIP the one per // client IP. PolicyCollect = "collect" PolicyCollectIP = "collect-ip" // PolicyForgotIP is the per-IP cap on forgot-password requests. PolicyForgotIP = "forgot-password-ip" // PolicyService is the budget of the internal listener's service endpoints per // Service credential. PolicyService = "service" )
Policy names. They are the `policy` label of ratelimit_rejected_total, so they must stay a small bounded set.
Variables ¶
This section is empty.
Functions ¶
func Rejected ¶
Rejected records one rejection under policy: ratelimit_rejected_total{policy} and a warn log. It carries the policy and the client address only, never an email or a path (paths hold tokens).
func WriteProblem ¶
func WriteProblem(w http.ResponseWriter)
WriteProblem renders the RFC 7807 429 body. Headers already on w (Retry-After, X-RateLimit-*, CORS) are kept; the Retry-After seconds are repeated in the body as retryAfter, so a client that only sees the body can show the wait.
Types ¶
type Exchange ¶
type Exchange struct {
// contains filtered or unexported fields
}
Exchange is one HTTP request's writer and request, carried in the context by Middleware so that code which receives only a ctx (the ogen security handlers, where the Workspace first becomes known) can apply the limiter. Its methods are safe on a nil Exchange (a ctx without Middleware), where they allow everything.
func FromContext ¶
FromContext returns the request's Exchange, nil outside Middleware.
func (*Exchange) AuthBlocked ¶
AuthBlocked refuses before a credential is even checked when the client address has already spent its failed-authentication budget, so guessing cannot continue past the limit.
func (*Exchange) AuthFailed ¶
AuthFailed counts one failed authentication against the client address. Only failures are counted, so legitimate traffic never spends this budget.
func (*Exchange) ChargeCollect ¶
ChargeCollect counts the request against the Workspace's /collect budget, a budget of its own apart from /api and /mcp. Like ChargeWorkspace it charges at most once per HTTP request.
func (*Exchange) ChargeService ¶
ChargeService counts the request against the Service credential's budget. Like the other charges it counts at most once per HTTP request.
func (*Exchange) ChargeWorkspace ¶
ChargeWorkspace counts the request against the Workspace budget, the burst and steady windows stacked. It charges at most once per HTTP request however many times the request is authenticated (/mcp authenticates its call again when it dispatches in-process to /api), so /api and /mcp draw one budget at one request each. Its X-RateLimit-* headers land on the shared writer and so on the successful response.
func (*Exchange) Delay ¶
func (e *Exchange) Delay(ctx context.Context, policy string, limit int, wait time.Duration, now time.Time) error
Delay refuses a request that must wait out a delay rather than a window budget (the per-account login delay, ADR 0025): the caller may retry `wait` from now. limit is the budget reported in X-RateLimit-Limit; Remaining is always 0. Like the other rejections it sets the 429 headers on the shared writer, counts and logs the rejection under policy, and returns the *LimitedError the error handler renders.
type LimitedError ¶
type LimitedError struct{ Policy string }
LimitedError is a rejection by a Policy. The 429 headers are already on the response writer and the rejection is counted and logged; whoever receives it only has to render the body with WriteProblem.
func (*LimitedError) Error ¶
func (e *LimitedError) Error() string
type Limiter ¶
type Limiter struct {
// contains filtered or unexported fields
}
Limiter holds every policy built from the configured limits.
func (*Limiter) Middleware ¶
Middleware applies the policy of the request's route. It sits after the client address middleware (the key is clientip, never a raw header) and before timeout. Operational endpoints are never limited, whatever policies exist.
func (*Limiter) RecordsTracking ¶
RecordsTracking reports whether the engagement of this request may be recorded: false once the client IP spent the tracking guard. The caller must still serve the pixel or the redirect; only the recording is skipped.
type Policy ¶
type Policy struct {
// contains filtered or unexported fields
}
Policy is one named limiter. The zero value and nil allow everything, which is what a disabled (0) limit builds.
func NewPolicy ¶
NewPolicy builds a limiter of limit requests per window. It returns nil when limit is not positive (disabled).
func (*Policy) Allow ¶
Allow counts one request against key and reports whether it may proceed. On a rejection it has already answered 429 (X-RateLimit-* and Retry-After headers, problem body), counted and logged it, and the caller must stop.
func (*Policy) Blocked ¶
Blocked reports, without counting, whether key has already spent its budget. It answers like Reject (429 headers, counted, logged), for limits that count only some outcomes (failed authentications) and must refuse before the work is done.
func (*Policy) Exceeded ¶
Exceeded counts one request against key and reports whether the budget is spent. Unlike Allow it never answers the request: it is for a recording guard, where the caller keeps serving the response and only skips the side effect (tracking never refuses a recipient, ADR 0025). An exceeded request is counted and logged.
func (*Policy) Reject ¶
Reject counts one request against key like Allow but leaves the response to the caller: it returns a *LimitedError (headers set, rejection counted and logged) when the budget is spent and nil otherwise. X-RateLimit-* headers are set on w either way, so they reach the successful response too.