Skip to main content

v1.1.0

Security, atomicity, REST compatibility, and deterministic behavior. Released on the main branch as Composer tag v1.1.0.

Upgrade summary

{
"require": {
"better-route/better-route": "^1.1"
}
}

1.1.0 is a hardening release across route intent, CORS, write safety, and the WooCommerce layer. Everything below is verified by the test suite (156 tests), PHPStan, and a live WordPress 7.0.1 / WooCommerce 10.9.4 HPOS install; CI now also runs PHP 8.3 and 8.4.

Like 1.0.0, this release contains intentional fail-safe behavior changes — the biggest one being deny-by-default for every HTTP method. Read the Behavior change checklist before upgrading.

Router

  • Deny-by-default for every method. Raw Router routes without an explicit permission callback now fail with 403 regardless of HTTP method — GET and OPTIONS included (write methods have denied since v0.4.0). Declare intent with ->permission(...), ->protectedByMiddleware(...), or ->publicRoute() on every route. See Router.
  • group() unwinds safely after exceptions — a throwing callback no longer corrupts the prefix/middleware stack for later registrations.
  • Handler resolution hardened. Static [Controller::class, 'method'] callables run without instantiation; union-typed context parameters resolve; nonexistent classes/methods and constructor-dependent handler classes fail with clear InvalidArgumentExceptions; handlers may require at most two parameters.
  • Registration fails loudly. WordPressRestDispatcher throws when register() runs outside rest_api_init or when WordPress core rejects a route.

CORS

  • WordPress CORS bridge. CorsMiddleware now registers matched routes with WordPressCorsBridge at Router::register() time. The bridge answers allowed/denied preflight on rest_pre_dispatch (before the route or its permission callback runs) and replaces WordPress core Access-Control-* headers on rest_pre_serve_request, so the configured allowlist is authoritative. Explicit Router::options() preflight routes are no longer required. See CORS.
  • Configuration validated at construction. Origins must be valid serialized origins; method and header names must be valid HTTP tokens; maxAgeSeconds must be non-negative — closing response-header injection through configuration.

Write safety

  • Atomic idempotency uses reservation leases. WpdbAtomicIdempotencyStore issues an unforgeable per-reservation lease token (LeaseAwareAtomicIdempotencyStoreInterface), creates/migrates its table via installSchema(), and stores responses through StoredResponseCodec — data-only serialization that never deserializes arbitrary classes. Request fingerprints are deep-canonical (Support\Canonicalizer), keys are bounded, and failed requests keep their reservation until TTL expiry by default, preventing an uncertain side effect from executing twice. ArrayAtomicIdempotencyStore is for tests only. See Atomic idempotency.
  • Optimistic locking gets a critical section. Version resolution and the write now run inside a per-resource MySQL advisory lock (WpdbOptimisticLockCriticalSection; pluggable via OptimisticLockCriticalSectionInterface / CallbackOptimisticLockCriticalSection). Writers outside this protocol can still race — use the same lock or a storage-level conditional UPDATE for external writers. See Optimistic locking.

Identity, auth, and rate limiting

  • Native WordPress identity scopes shared state. A logged-in WP user now scopes cache, idempotency, and rate-limit keys even without an auth middleware attribute (Support\RequestIdentity); HMAC identities populate the shared auth context.
  • JWT max-lifetime requires both iat and exp. With maxLifetimeSeconds configured, tokens missing either claim are rejected (previously a missing iat skipped the lifetime check).
  • JWKS refresh hardened. Unknown-key refreshes are throttled, and a failed fetch preserves the last-known-good key set.
  • Rate limiting cannot silently degrade. WpObjectCacheRateLimiter throws at construction without a persistent external object cache and atomic wp_cache_incr() (and refuses to reset an existing counter — no bypass on eviction). TransientRateLimiter serializes its read-modify-write with a MySQL advisory lock in the default WordPress configuration. 429 responses include Retry-After and rate-limit headers.

Resources

  • CPT reads fail closed on missing/private/password-protected visibility data.
  • Truthful visibility pagination. A custom cptVisibilityPolicy() callback is evaluated item by item; the Resource layer scans all matching repository pages before slicing so total/pagination stay correct. For large datasets, express visibility as a query-level filter instead. Invariant WP query arguments can no longer be overridden through filters. See CPT resource.
  • Table resources: SQL NULL writes are real SQL nulls, default ordering uses the primary key, and list ordering has an ID tie-breaker for deterministic pagination.

WooCommerce

  • Order writes are transactional and fully validated. Payloads are validated before persistence (typed scalars, existing customer_id, line-item and address fields reject unknown keys), and create/update run inside wc_transaction_query. See Orders.
  • Strict product/customer/coupon validation, aligned OpenAPI input schemas (price removed from WooProductInput; new WooCustomerCreateInput / WooCouponCreateInput), and an ID tie-breaker on all list ordering.
  • Coupon code writes are uniqueness-locked. Updates enforce code uniqueness (409 coupon_exists) under a per-code MySQL advisory lock; a lock timeout returns 409 coupon_write_in_progress.
  • Durable idempotency by default. Without an explicit store the Woo registrar installs and uses the lease-aware wpdb atomic store (idempotency now also covers customer/coupon writes, not just orders/products). A schema/install failure is reported instead of degrading to a request-local store; a versioned option (better_route_atomic_idempotency_schema_version) prevents repeated schema checks. See Configuration.
  • 'actions' => [] disables a resource. An empty action list now registers no routes for that resource (previously it silently fell back to the full set); invalid action values throw.
  • WordPress global REST params accepted. _fields, _locale, _embed, _envelope, _jsonp no longer trip the unknown-parameter check.

OpenAPI

  • Parameters derive from route args. Executable route args are exported automatically as path/query parameters; explicit meta.parameters entries override derived entries with the same name and location.
  • OPTIONS operations are documented as 204; custom meta.responses replace the defaults.
  • Resource create/update responses are { "data": ... } envelopes — strict component sets should provide <Resource>Response alongside <Resource>, <Resource>Input, and <Resource>ListResponse. See OpenAPI.

Errors and observability

  • Response/error headers are validated against header injection; WP_Error details are allowlisted before entering the error envelope.
  • Audit and metric sink failures are best-effort and never change the API response; PrometheusMetricSink / InMemoryMetricSink are documented as process/request-local collectors.

Files added

  • src/Middleware/Cors/WordPressCorsBridge.php — preflight + authoritative CORS headers via WP filters.
  • src/Middleware/WordPressRouteMiddlewareInterface.php — middleware notified of matched routes at register() time.
  • src/Middleware/Write/LeaseAwareAtomicIdempotencyStoreInterface.php, src/Middleware/Write/StoredResponseCodec.php — reservation leases + data-only response storage.
  • src/Middleware/Write/OptimisticLockCriticalSectionInterface.php, src/Middleware/Write/WpdbOptimisticLockCriticalSection.php, src/Middleware/Write/CallbackOptimisticLockCriticalSection.php — optimistic-lock critical sections.
  • src/Support/Canonicalizer.php, src/Support/RequestIdentity.php, src/Support/RestRequestParameters.php — canonical fingerprints, native-identity scoping, WP global REST params.
  • tests/WooRouteRegistrarTest.php, tests/PassthroughOptimisticLockCriticalSection.php — new coverage (156 tests total).

Behavior change checklist

1.1.0 is safe to adopt, but these consumer-visible changes are intentional:

ChangeAction
Every raw Router route denies by default (GET/OPTIONS included)Add ->publicRoute(), ->permission(...), or ->protectedByMiddleware(...) to every route — previously-public GET routes without intent now return 403
Preflight is answered by the WordPress CORS bridgeRemove explicit Router::options() preflight routes, or add explicit intent to the ones you keep
WP core CORS headers are replaced on matched routesThe CorsPolicy allowlist is authoritative — verify it lists every origin you serve
register() outside rest_api_init throwsWrap Router::register() in an add_action('rest_api_init', ...) callback
Failed idempotent requests stay reserved until TTLRetrying a failed write with the same key is refused until the reservation expires — issue a new key to retry deliberately
ArrayAtomicIdempotencyStore is tests-onlyUse the wpdb store (or your own) in production
WpObjectCacheRateLimiter throws without a persistent object cacheUse TransientRateLimiter on default hosting
JWT max-lifetime requires iat and expTokens missing iat are rejected when maxLifetimeSeconds is set — ensure the issuer emits both claims
Woo 'actions' => [] disables the resourceOmit the key for the full route set; empty array now means "no routes"
Woo idempotency store is durable by defaultThe wpdb store's table is installed automatically; install failures surface as errors instead of silent request-local fallback
Unknown nested keys in Woo order/customer payloads return 400Strip unrecognized line-item/address fields from clients
Coupon-code update conflicts return 409Handle coupon_exists / coupon_write_in_progress on coupon writes

Compared to 1.0.0

  • 1.0.0 hardened the identity-boundary defaults and fixed WooCommerce semantics.
  • 1.1.0 closes the remaining default-permissiveness gaps (public-by-default GET/OPTIONS, WP core CORS headers, racy rate-limit fallbacks, request-local Woo idempotency) and makes concurrent writes atomic end to end (reservation leases, optimistic-lock critical sections, transactional Woo order writes, uniqueness-locked coupon codes).