Files
skills/docs/api-stack/api/api-plugin-rate-limit/src/middleware/rate-limit.md
2026-05-29 08:20:56 +01:00

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-IP for IP, X-Tenant-ID header 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.
  • onDenied hook 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 from service.config when not overridden.
  • function rateLimitExceeded(decision: RateLimitDecision): RateLimitPluginError — construct a typed LIMIT_EXCEEDED error 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 RateLimitPluginError into Problem+JSON would get a double translation otherwise. Callers wiring service.check manually can use rateLimitExceeded(decision) to surface the same error through their handler chain.
  • Header bag is merged onto c.res.headers both before the denial response and after next() returns — Hono sometimes swaps the res between phases, so both writes are required.
  • The default IP resolver respects X-Forwarded-For (first hop) and falls back to X-Real-IP. Operators behind a trusted proxy should override this with a header their proxy guarantees (e.g. CF-Connecting-IP on Cloudflare).
  • The default tenant resolver reads X-Tenant-ID — consumers using JWT-derived tenant ids should override resolveTenantId to read from c.get("user").