3.7 KiB
3.7 KiB
middleware/rate-limit — Hono middleware wrapping the rate-limit service
Purpose
Constructs a Hono MiddlewareHandler that consults a
RateLimitService on every request, emits the configured
X-RateLimit-* / RateLimit-* headers on every response (allowed AND
denied), and on denial writes an RFC 7807 application/problem+json
body with the 429 status. The middleware is the canonical mount point
operators wire into their app — app.use("*", rateLimit({ service }))
covers every route; selective mounts let consumers apply different
policies per path prefix.
The middleware also publishes the resolved RateLimitDecision to the
Hono context (c.set("rateLimit", decision)) so downstream handlers
can inspect the matched route, the remaining budget, or the resolved
tenant id without re-running the check.
Features
- Per-request IP and tenant resolution via injectable resolvers
(defaults:
X-Forwarded-For/X-Real-IPfor IP,X-Tenant-IDheader for tenant). - Header bag merged onto the outgoing response after
next()so downstream handlers cannot accidentally strip them. - RFC 7807 Problem+JSON body on 429.
onDeniedhook fires before the response is written — useful for custom audit logging.- Decision attached to the Hono context so handlers can act on it.
Usage: Basic
import { Hono } from "hono";
import { rateLimit } from "@suaveplan/api-plugin-rate-limit";
const app = new Hono();
app.use("*", rateLimit({ service }));
Usage: Advanced
import { Hono } from "hono";
import {
rateLimit,
type RateLimitDecision,
} from "@suaveplan/api-plugin-rate-limit";
const app = new Hono();
app.use(
"/api/*",
rateLimit({
service,
headerStyle: "both",
emitRetryAfter: true,
resolveIp: (c) => c.req.header("CF-Connecting-IP") ?? "unknown",
resolveTenantId: (c) => c.get("organisation")?.id,
onDenied: (decision: RateLimitDecision) => {
auditLog.warn("rate-limit denied", {
routeId: decision.matchedRouteId,
key: decision.key,
});
},
})
);
app.get("/api/users", (c) => {
const decision = c.get("rateLimit");
return c.json({ remaining: decision.remaining });
});
API Reference
interface RateLimitMiddlewareOptions—{ service, headerStyle?, emitRetryAfter?, resolveTenantId?, resolveIp?, onDenied? }.function rateLimit(options: RateLimitMiddlewareOptions): MiddlewareHandler— returns the Hono middleware. Defaults pulled fromservice.configwhen not overridden.function rateLimitExceeded(decision: RateLimitDecision): RateLimitPluginError— construct a typedLIMIT_EXCEEDEDerror for callers that want to throw rather than return a Response.
Implementation Notes
- The middleware returns the 429 response inline rather than throwing
the typed error — operators who installed an error-handler
translating
RateLimitPluginErrorinto Problem+JSON would get a double translation otherwise. Callers wiringservice.checkmanually can userateLimitExceeded(decision)to surface the same error through their handler chain. - Header bag is merged onto
c.res.headersboth before the denial response and afternext()returns — Hono sometimes swaps theresbetween phases, so both writes are required. - The default IP resolver respects
X-Forwarded-For(first hop) and falls back toX-Real-IP. Operators behind a trusted proxy should override this with a header their proxy guarantees (e.g.CF-Connecting-IPon Cloudflare). - The default tenant resolver reads
X-Tenant-ID— consumers using JWT-derived tenant ids should overrideresolveTenantIdto read fromc.get("user").