Middleware Catalog
Auth
JwtAuthMiddleware— HS256 / asymmetric JWT verification through anyJwtVerifierInterface; (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 withHttpJwksProvider/StaticJwksProviderBearerTokenAuthMiddleware— (v1.0.0) sameallowGrantedScopeWildcardsopt-in asJwtAuthMiddlewareHmacSignatureMiddleware(v0.6.0) — HMAC request signatures with replay window; pairs withHmacSecretProviderInterface/ArrayHmacSecretProvider; (v1.0.0)signQueryString(default false) also signs the canonical query stringCookieNonceAuthMiddlewareApplicationPasswordAuthMiddlewareWpClaimsUserMapper— (v1.0.0)email/loginclaim mapping is off by default; email mapping requires anemail_verifiedclaim (requireEmailVerified, default true)OwnershipGuardMiddleware(v0.5.0) — route-level "current user owns this resource" guard; pairs withOwnedResourcePolicy::currentUserOwns()for Resource DSL
Write safety
IdempotencyMiddleware— response-replay cache (handler runs, then store writes)TransientIdempotencyStoreWpdbIdempotencyStore(v0.3.0) —wpdb-backed replay store; callinstallSchema()once on activationAtomicIdempotencyMiddleware(v0.5.0) — reserves the key before handler execution; blocks concurrent retries with409 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 viaStoredResponseCodec, and a failed request stays reserved until its TTL expires by default (releaseOnThrowable: trueopts into releasing the reservation on error;maxKeyLengthdefaults 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/releaseStoredResponseCodec(v1.1.0) — data-only replay-response serialization; never unserializes arbitrary classesArrayAtomicIdempotencyStore(v0.5.0) — in-memory store, tests only (v1.1.0)WpdbAtomicIdempotencyStore(v0.5.0) —wpdbINSERT IGNOREreservation store; dedicated table,installSchema()once on activation; (v1.1.0) lease-aware, andinstallSchema()also migrates older tablesSingleUseTokenMiddleware(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 testsWpdbSingleUseTokenStore(v0.6.0) —wpdb-backed token store with TTL pruning;installSchema()once on activationWpCacheSingleUseTokenStore(v0.6.0) — object-cache lock + transient-backed records; (v1.0.0) requires a persistent object cache (throws otherwise) — useWpdbSingleUseTokenStoreon default hostingOptimisticLockMiddleware— (v1.0.0) a missing precondition returns428 precondition_required(was412); (v1.1.0) version resolution + write run inside a critical section so cooperating Better Route writers cannot race between the version check and the writeCallbackOptimisticLockVersionResolverOptimisticLockCriticalSectionInterface(v1.1.0) — critical-section contract for the optimistic-lock write windowWpdbOptimisticLockCriticalSection(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 aCorsPolicy, short-circuits preflightOPTIONSwith204; (v1.1.0) implementsWordPressRouteMiddlewareInterface, soRouter::register()installs the WordPress CORS bridge for every route it is attached to; origins, methods, and header names are validated against response-header injectionBetterRoute\Middleware\Cors\CorsPolicy— origin allowlist, methods/headers/exposed-headers, credentials, max ageBetterRoute\Middleware\Cors\WordPressCorsBridge(v1.1.0) — installed automatically whenCorsMiddlewareis attached; handles allowed/denied preflight onrest_pre_dispatch(priority 9) before WordPress dispatches the route, and replaces WordPress core CORS headers onrest_pre_serve_request(priority 20) so the configured allowlist stays authoritative for matched routesBetterRoute\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), andOPTIONSroutes 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; implementsClientIpResolverInterfaceBetterRoute\Middleware\Network\ClientIpResolverInterface— minimalresolve(?mixed $request = null): ?stringcontractBetterRoute\Middleware\Network\IpAllowlistMiddleware— denies requests outside an IPv4/IPv6 CIDR allowlistBetterRoute\Middleware\Network\CidrMatcher— IPv4/IPv6 aware CIDR / single-host matcher
Rate limiting
RateLimitMiddleware— (v0.6.0)clientIpResolvernow accepts eitherHttp\ClientIpResolverorMiddleware\Network\ClientIpResolverInterface; (v1.1.0)429responses carryRetry-AfterandX-RateLimit-Limit/X-RateLimit-Remaining/X-RateLimit-Resetheaders;limit/windowSecondsbelow 1 throwTransientRateLimiter— (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; customgetTransient/setTransientcallables accept an optionalsynchronizecallable for the same guaranteeWpObjectCacheRateLimiter(v0.3.0) — uses the WP object cache; throwsRuntimeExceptionifwp_cache_*is unavailable; (v1.1.0) requires a persistent external object cache with atomicwp_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_Errorand error statuses are never stored; cache keys use the canonical request identity (see below)TransientCacheStoreETagMiddleware(v0.3.0) — emitsETagheaders and replies304 Not ModifiedonIf-None-Matchmatches (GET/HEAD only); (v1.1.0) preserves WordPress REST status/data/headers (sets the header on aWP_REST_Responseinstead of unwrapping it), passesWP_Errorthrough untouched, supports weak validators andIf-None-Matchlists (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 toTrustedProxyClientIpResolver. Constructor andresolve(?array $server = null)API unchanged. New code should preferTrustedProxyClientIpResolverdirectly.BetterRoute\Http\OAuthErrorNormalizer(v0.6.0) — emits OAuth RFC 6749 style error bodies when a route opts in viameta(['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 trip400 validation_failed.
Observability
AuditMiddleware— (v0.5.0) now mergesRequestContext::$attributes['audit']into emitted eventsAuditEnricherMiddleware(v0.5.0) — adds auth provider/user/subject, hashed idempotency key, optional client IP, and static fields to theauditattributeErrorLogAuditLoggerMetricsMiddleware— (v1.1.0) sink failures are swallowed so telemetry can never mask the application result; the metric prefix is validated as a Prometheus name prefixInMemoryMetricSink— process/request-local; export or replace with a persistent backend for cross-request totalsPrometheusMetricSink— 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:
- CORS (preflight short-circuit before anything else)
- IP allowlist (drop unauthorized networks early)
- Metrics/Audit (outer visibility)
- Rate limit
- Auth (JWT, HMAC, cookie/nonce, application password)
- Ownership / single-use token / cache / idempotency / optimistic lock
- 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:
auth.userId > 0→ hashedidentity:{sha256}key forprovider:user:{userId}auth.subject(non-empty) → hashed key forprovider:subject:{subject}- (v1.1.0)
attributes['userId']or a logged-in WordPress user (get_current_user_id()) → hashed key forwordpress:user:{id}— a native WP identity now scopes keys even without an auth middleware attached - (v1.1.0) HMAC key id from
attributes['hmac']→ hashed key forhmac:key:{keyId} RateLimitMiddlewareonly: client IP fallback →"ip:{clientIp}"- 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.