Skip to main content

Middleware Catalog

Auth

  • JwtAuthMiddleware — HS256 / asymmetric JWT verification through any JwtVerifierInterface; (v1.0.0) allowGrantedScopeWildcards (default false) opts into trailing-* wildcards on granted scopes (required-scope wildcards are unaffected)
  • Rs256JwksJwtVerifier (v0.6.0) — RS256 / ES256 verifier backed by JWKS; pairs with HttpJwksProvider / StaticJwksProvider
  • BearerTokenAuthMiddleware(v1.0.0) same allowGrantedScopeWildcards opt-in as JwtAuthMiddleware
  • HmacSignatureMiddleware (v0.6.0) — HMAC request signatures with replay window; pairs with HmacSecretProviderInterface / ArrayHmacSecretProvider; (v1.0.0) signQueryString (default false) also signs the canonical query string
  • CookieNonceAuthMiddleware
  • ApplicationPasswordAuthMiddleware
  • WpClaimsUserMapper(v1.0.0) email/login claim mapping is off by default; email mapping requires an email_verified claim (requireEmailVerified, default true)
  • OwnershipGuardMiddleware (v0.5.0) — route-level "current user owns this resource" guard; pairs with OwnedResourcePolicy::currentUserOwns() for Resource DSL

Write safety

  • IdempotencyMiddleware — response-replay cache (handler runs, then store writes)
  • TransientIdempotencyStore
  • WpdbIdempotencyStore (v0.3.0)wpdb-backed replay store; call installSchema() once on activation
  • AtomicIdempotencyMiddleware (v0.5.0) — reserves the key before handler execution; blocks concurrent retries with 409 idempotency_in_progress; (v1.1.0) keys are bounded/validated (400 idempotency_key_invalid), request fingerprints are deep-canonical, stored responses are serialized data-only via StoredResponseCodec, and a failed request stays reserved until its TTL expires by default (releaseOnThrowable: true opts into releasing the reservation on error; maxKeyLength defaults to 200)
  • AtomicIdempotencyStoreInterface (v0.5.0) — store contract (reserve / complete / release)
  • LeaseAwareAtomicIdempotencyStoreInterface (v1.1.0) — extends reservations with an unforgeable per-reservation lease token so only the reserving request can complete/release
  • StoredResponseCodec (v1.1.0) — data-only replay-response serialization; never unserializes arbitrary classes
  • ArrayAtomicIdempotencyStore (v0.5.0) — in-memory store, tests only (v1.1.0)
  • WpdbAtomicIdempotencyStore (v0.5.0)wpdb INSERT IGNORE reservation store; dedicated table, installSchema() once on activation; (v1.1.0) lease-aware, and installSchema() also migrates older tables
  • SingleUseTokenMiddleware (v0.6.0) — atomic one-time token consumption (OAuth codes, magic links, password resets)
  • SingleUseTokenStoreInterface (v0.6.0) — store contract (consume / store / wasConsumed)
  • ArraySingleUseTokenStore (v0.6.0) — in-memory store for tests
  • WpdbSingleUseTokenStore (v0.6.0)wpdb-backed token store with TTL pruning; installSchema() once on activation
  • WpCacheSingleUseTokenStore (v0.6.0) — object-cache lock + transient-backed records; (v1.0.0) requires a persistent object cache (throws otherwise) — use WpdbSingleUseTokenStore on default hosting
  • OptimisticLockMiddleware(v1.0.0) a missing precondition returns 428 precondition_required (was 412); (v1.1.0) version resolution + write run inside a critical section so cooperating Better Route writers cannot race between the version check and the write
  • CallbackOptimisticLockVersionResolver
  • OptimisticLockCriticalSectionInterface (v1.1.0) — critical-section contract for the optimistic-lock write window
  • WpdbOptimisticLockCriticalSection (v1.1.0) — default WordPress implementation; per-resource MySQL advisory lock (GET_LOCK)
  • CallbackOptimisticLockCriticalSection (v1.1.0) — wrap a custom locking scheme (or pass-through for single-writer setups)

Public-client / CORS (v0.5.0)

  • BetterRoute\Middleware\Cors\CorsMiddleware — applies a CorsPolicy, short-circuits preflight OPTIONS with 204; (v1.1.0) implements WordPressRouteMiddlewareInterface, so Router::register() installs the WordPress CORS bridge for every route it is attached to; origins, methods, and header names are validated against response-header injection
  • BetterRoute\Middleware\Cors\CorsPolicy — origin allowlist, methods/headers/exposed-headers, credentials, max age
  • BetterRoute\Middleware\Cors\WordPressCorsBridge (v1.1.0) — installed automatically when CorsMiddleware is attached; handles allowed/denied preflight on rest_pre_dispatch (priority 9) before WordPress dispatches the route, and replaces WordPress core CORS headers on rest_pre_serve_request (priority 20) so the configured allowlist stays authoritative for matched routes
  • BetterRoute\Middleware\WordPressRouteMiddlewareInterface (v1.1.0) — implemented by middleware that needs a WordPress-level hook per registered route (registerWordPressRoute($namespace, $route))
  • Router::options() — register explicit preflight routes when you need a custom preflight handler; (v1.1.0) no longer required for CORS (the bridge answers preflight), and OPTIONS routes now deny by default like every other method — declare intent explicitly

Network (v0.6.0)

  • BetterRoute\Middleware\Network\TrustedProxyClientIpResolver — trusted-proxy aware client IP resolution; implements ClientIpResolverInterface
  • BetterRoute\Middleware\Network\ClientIpResolverInterface — minimal resolve(?mixed $request = null): ?string contract
  • BetterRoute\Middleware\Network\IpAllowlistMiddleware — denies requests outside an IPv4/IPv6 CIDR allowlist
  • BetterRoute\Middleware\Network\CidrMatcher — IPv4/IPv6 aware CIDR / single-host matcher

Rate limiting

  • RateLimitMiddleware(v0.6.0) clientIpResolver now accepts either Http\ClientIpResolver or Middleware\Network\ClientIpResolverInterface; (v1.1.0) 429 responses carry Retry-After and X-RateLimit-Limit / X-RateLimit-Remaining / X-RateLimit-Reset headers; limit/windowSeconds below 1 throw
  • TransientRateLimiter(v1.1.0) in its default WordPress-transient configuration the counter update runs under a MySQL advisory lock (GET_LOCK), so concurrent hits cannot lose increments; custom getTransient/setTransient callables accept an optional synchronize callable for the same guarantee
  • WpObjectCacheRateLimiter (v0.3.0) — uses the WP object cache; throws RuntimeException if wp_cache_* is unavailable; (v1.1.0) requires a persistent external object cache with atomic wp_cache_incr() and throws instead of silently degrading to a racy read/modify/write

Caching

  • CachingMiddleware(v1.1.0) only 2xx responses are cached; WP_Error and error statuses are never stored; cache keys use the canonical request identity (see below)
  • TransientCacheStore
  • ETagMiddleware (v0.3.0) — emits ETag headers and replies 304 Not Modified on If-None-Match matches (GET/HEAD only); (v1.1.0) preserves WordPress REST status/data/headers (sets the header on a WP_REST_Response instead of unwrapping it), passes WP_Error through untouched, supports weak validators and If-None-Match lists (weak comparison per RFC 9110), and sanitizes custom resolver tags against header injection

HTTP infrastructure

  • BetterRoute\Http\ClientIpResolver — kept stable since v0.3.0; (v0.6.0) now delegates internally to TrustedProxyClientIpResolver. Constructor and resolve(?array $server = null) API unchanged. New code should prefer TrustedProxyClientIpResolver directly.
  • BetterRoute\Http\OAuthErrorNormalizer (v0.6.0) — emits OAuth RFC 6749 style error bodies when a route opts in via meta(['error_format' => 'oauth_rfc6749']). See OAuth Error Format.

Support utilities (v0.6.0)

  • BetterRoute\Support\Crypto — CSPRNG token generation, hex/base64/base64url encoding, strict base64url decoding, constant-time compare. See Crypto Utilities.
  • BetterRoute\Support\CryptoEncoding — enum (Hex, Base64, Base64Url).
  • BetterRoute\Support\Canonicalizer (v1.1.0) — deterministic deep-canonical JSON encoding; used for cache/idempotency/rate-limit keys and request fingerprints.
  • BetterRoute\Support\RequestIdentity (v1.1.0) — shared identity-key derivation for the keyed middlewares (see "Default keys" below).
  • BetterRoute\Support\RestRequestParameters (v1.1.0) — allowlists the WordPress global REST query parameters (_locale, _fields, _embed, _envelope, _jsonp) next to endpoint parameters in strict list-query parsing, so they no longer trip 400 validation_failed.

Observability

  • AuditMiddleware(v0.5.0) now merges RequestContext::$attributes['audit'] into emitted events
  • AuditEnricherMiddleware (v0.5.0) — adds auth provider/user/subject, hashed idempotency key, optional client IP, and static fields to the audit attribute
  • ErrorLogAuditLogger
  • MetricsMiddleware(v1.1.0) sink failures are swallowed so telemetry can never mask the application result; the metric prefix is validated as a Prometheus name prefix
  • InMemoryMetricSink — process/request-local; export or replace with a persistent backend for cross-request totals
  • PrometheusMetricSink — process/request-local collector (same caveat)
  • AuditEventFactory

Typical global stack

$router->middleware([
new MetricsMiddleware(new PrometheusMetricSink()),
new AuditMiddleware(new ErrorLogAuditLogger()),
new RateLimitMiddleware(new TransientRateLimiter(), limit: 100, windowSeconds: 60),
]);

Order recommendation:

  1. CORS (preflight short-circuit before anything else)
  2. IP allowlist (drop unauthorized networks early)
  3. Metrics/Audit (outer visibility)
  4. Rate limit
  5. Auth (JWT, HMAC, cookie/nonce, application password)
  6. Ownership / single-use token / cache / idempotency / optimistic lock
  7. business handler

Default keys (v0.3.0, revised v1.1.0)

CachingMiddleware, IdempotencyMiddleware, and RateLimitMiddleware derive default keys from request identity. Since v1.1.0 the identity comes from Support\RequestIdentity and falls through in this order:

  1. auth.userId > 0 → hashed identity:{sha256} key for provider:user:{userId}
  2. auth.subject (non-empty) → hashed key for provider:subject:{subject}
  3. (v1.1.0) attributes['userId'] or a logged-in WordPress user (get_current_user_id()) → hashed key for wordpress:user:{id} — a native WP identity now scopes keys even without an auth middleware attached
  4. (v1.1.0) HMAC key id from attributes['hmac'] → hashed key for hmac:key:{keyId}
  5. RateLimitMiddleware only: client IP fallback → "ip:{clientIp}"
  6. otherwise → "guest"

Composite keys are canonical-JSON encoded (Support\Canonicalizer), so key stability no longer depends on parameter order. Upgrading from any earlier version invalidates previously stored keys once — expect a one-time cache miss. Pass an explicit keyResolver to keep keys stable across upgrades.