baukit-http 0.10.29

Shared Axum middleware, error, and HTTP telemetry conventions.
Documentation
# Changelog

All notable changes to `baukit-http` are documented here.

## [Unreleased]

## [0.10.29] - 2026-10-15

## [0.10.28] - 2026-10-14

## [0.10.27] - 2026-10-14

## [0.10.26] - 2026-10-13

## [0.10.25] - 2026-10-13

## [0.10.24] - 2026-10-13

### Changed

- Every `5xx` response with a valid `Retry-After` now classifies as `RetryAfter`,
  including `504`. Delta-seconds and HTTP dates respect the configured delay cap.
  Without a usable delay, `504` stays `Timeout` and other `5xx` stay `Unavailable`.
  `401` and `403` keep their existing behavior.

## [0.10.23] - 2026-10-12

## [0.10.22] - 2026-10-12

## [0.10.21] - 2026-10-12

## [0.10.20] - 2026-10-12

## [0.10.19] - 2026-10-11

## [0.10.18] - 2026-10-11

## [0.10.17] - 2026-10-11

## [0.10.16] - 2026-10-11

## [0.10.15] - 2026-10-11

## [0.10.14] - 2026-10-11

## [0.10.13] - 2026-10-10

## [0.10.12] - 2026-10-10

## [0.10.11] - 2026-10-10

### Added

- `RetryHeaderOptions::with_forbidden_rate_limit` opts in to retryable `403` rate limits.
  Usable retry headers give `RetryAfter`. Otherwise, `x-ratelimit-remaining: 0` gives
  `RetryAfter` for a valid `x-ratelimit-reset` epoch, or `RateLimited` without one.
  Past resets give zero delay, and all delays respect `max_retry_after`.
  The default policy and `401` classification stay unchanged.
- `classify_http_status_with_options_at` accepts a reference time for HTTP dates and reset epochs.

## [0.10.10] - 2026-10-10

## [0.10.9] - 2026-10-10

## [0.10.8] - 2026-10-09

## [0.10.7] - 2026-10-09

## [0.10.6] - 2026-10-09

## [0.10.5] - 2026-10-08

## [0.10.4] - 2026-10-08

## [0.10.3] - 2026-10-08

## [0.10.2] - 2026-10-08

## [0.10.1] - 2026-10-08

## [0.10.0] - 2026-10-08

## [0.9.0] - 2026-10-07

## [0.8.0] - 2026-10-07

## [0.7.4] - 2026-10-06

## [0.7.3] - 2026-10-05

## [0.7.2] - 2026-10-04

## [0.7.1] - 2026-10-04

## [0.7.0] - 2026-10-04

### Changed

- Use caret requirements for third-party Rust dependencies so products can take compatible
  updates. Keep Baukit crate versions exact.

## [0.6.0] - 2026-10-02

## [0.5.2] - 2026-09-30

## [0.5.1] - 2026-09-29

## [0.5.0] - 2026-09-28

### Added

- `HttpOptions::with_additional_exposed_headers` adds response headers to the CORS exposed set,
  next to `with_additional_allowed_headers`. Invalid names return
  `HttpOptionsError::InvalidHeaderName` and duplicates are ignored.
- `ResponseCachePolicy` and `HttpOptions::with_response_cache_policy` control the default
  `Cache-Control` header. `ResponseCachePolicy::HandlerOwned` turns the default off.
- Strong-ETag revision preconditions. `RevisionEtag` formats `"<prefix><revision>"` ETags and
  parses exactly one strong `If-Match` validator through `required_if_match` or
  `optional_if_match`. `Revision` holds a revision in `0..=i64::MAX`. `InvalidIfMatch` names each
  rejected form: repeated header, empty, non-ASCII, `*`, weak, list, malformed, prefix mismatch,
  non-canonical revision, and out-of-range revision. `PreconditionError` converts into `ApiError`
  as 428 `precondition_required`, 400 `invalid_if_match` with `details.reason`, or 412
  `precondition_failed` with `details.currentRevision` when the stored revision is known.
  `ensure_current_revision` builds the 412 from an expected and a current revision.
  `RevisionOutOfRange` converts into a 500. Shared vectors live in
  `fixtures/etag-preconditions/vectors-v1.json`.
- `Idempotency-Key` parsing. `IdempotencyKeyRule::new(min, max)` and `try_new` set per-route
  bounds up to `MAX_IDEMPOTENCY_KEY_BYTES` (255) over visible ASCII, and `required` or `optional`
  read the header from a `HeaderMap`. `IdempotencyKey` hides its value from `Debug`.
  `InvalidIdempotencyKey` names each rejected form: repeated header, empty, invalid character, too
  short, and too long. `IdempotencyError` converts into `ApiError` as 400
  `idempotency_key_required`, 400 `invalid_idempotency_key` with `details.reason`, 409
  `idempotency_key_reused`, or 409 `idempotency_key_in_progress`. `IDEMPOTENCY_KEY` and the four
  code constants are exported.
- `RetryHeaderOptions::with_max_retry_after` clamps parsed `Retry-After` and vendor delays to a
  maximum. The default stays uncapped, so `classify_http_status` still reports what the upstream
  sent.

### Changed

- Breaking: the default CORS exposed set now also contains `Retry-After`, `RateLimit-Limit`,
  `RateLimit-Remaining`, and `RateLimit-Reset`, the headers `baukit-ratelimit` emits. Browsers can
  read them on responses that pass through `finalize` or `layers`.
- Breaking: every response from `finalize` or `layers` gets `Cache-Control: private, no-store`
  unless the handler or an inner layer already set `Cache-Control`. This includes error, timeout,
  panic, 404, and preflight responses. Set `ResponseCachePolicy::HandlerOwned` to keep the previous
  behavior.
- Breaking: the default CORS exposed set now also contains `ETag` and `Location`, and the default
  allowed request headers now contain `If-Match`.
- Breaking: the default allowed request headers now contain `Idempotency-Key`.
- The `HttpOptionsError::InvalidHeaderName` message now reads `invalid CORS header name`, because
  it covers exposed headers too.
- `baukit-http` depends on `baukit-core` with the `pagination` feature and no longer depends on
  `base64` or `ring` directly.
- Break: the error envelope field `request_id` is now `requestId`, through the re-exported
  `ErrorBody`. This covers every `ApiError`, extractor rejection, timeout, panic, body-limit,
  404, and 405 response.
- Break: the `RequestLocale` rejection detail key `accept_language` is now `acceptLanguage`.
- Break: `Page` responses carry `nextCursor` instead of `next_cursor`, through `baukit-core`.
- Break: `classify_http_status` maps `425 Too Early` to `RetryClass::Unavailable` instead of
  `Permanent`, so it is retryable. RFC 8470 section 5.2 lets a client retry once the request is no
  longer sent as early data.

### Removed

- Breaking: the `pagination` module and the root re-exports of `Cursor`, `Page`, `PageKey`,
  `PageParams`, `PaginationError`, `DEFAULT_PAGE_LIMIT`, and `MAX_PAGE_LIMIT`. They moved to
  `baukit_core::pagination` with no re-export at the old path. `From<PaginationError> for ApiError`
  stays in this crate.

### Migration

- Add `baukit-core` with `features = ["pagination"]` to every crate that imports the pagination
  types, and replace `baukit_http::{Cursor, Page, ...}` and `baukit_http::pagination::...` with
  `baukit_core::pagination::...`.
- Delete product middleware that overwrites `Access-Control-Expose-Headers` to add `Retry-After`
  or `RateLimit-*`. Pass any other product headers to `with_additional_exposed_headers`.
- Delete product middleware that sets `Cache-Control: private, no-store` or `no-store` on every
  response. Routes that need a different policy set `Cache-Control` in the handler, which wins.
- Apply `finalize` after authentication and rate-limit layers so their rejections carry CORS
  headers.
- Remove `etag`, `location`, and `if-match` from product `with_additional_exposed_headers` and
  `with_additional_allowed_headers` calls; duplicates are ignored, so leaving them is harmless.
- Replace product `If-Match` parsers and ETag formatters with `RevisionEtag`, keeping the product's
  prefix. Clients see 400 `invalid_if_match`, 412 `precondition_failed`, and 428
  `precondition_required` in place of product-specific codes.
- Remove `idempotency-key` from product `with_additional_allowed_headers` calls. Replace product
  `Idempotency-Key` parsers with an `IdempotencyKeyRule` whose bounds keep every key clients send
  today, and map key reuse to `IdempotencyError::Reused`. Clients see 400
  `idempotency_key_required`, 400 `invalid_idempotency_key`, and 409 `idempotency_key_reused` in
  place of product-specific codes.
- Clients that stop on `RetryClass::Permanent` now retry a `425`. Clients that schedule their own
  retries from `RetryClass::RetryAfter` should set `with_max_retry_after`.

## [0.4.0] - 2026-09-12

## [0.3.0] - 2026-09-04

### Added

- `RequestLocale` selects from a product-owned locale set using a percent-decoded query override or
  quality-weighted `Accept-Language`. Configuration fixes the fallback and query rule. Malformed,
  duplicate override, unsupported explicit, and oversized inputs return stable validation errors.
- `JsonRejectionCodes` and `HttpOptions::with_json_rejection_codes` preserve JSON rejection classes.
  Oversized bodies return 413, missing or invalid content types return 415, malformed JSON returns
  400, and data-shape errors return 422. Responses contain fixed safe text without submitted body or
  parser details.

### Compatibility

- Request locale extraction is additive. Existing handlers retain their current behavior until they
  put `RequestLocaleConfig` in Axum state and use the extractor.
- `HttpOptions::default()` and `with_json_rejection_code` retain the previous single-code 400
  response for `ApiJson<T>` rejections during this release cycle. See the README migration section
  before opting into class-specific responses.

## [0.2.1] - 2026-09-03

## [0.2.0] - 2026-09-03

## [0.1.2] - 2026-09-01

### Fixed

- `ApiError` stores response headers behind a lazily allocated box so the type stays at 104 bytes and
  does not trigger `clippy::result_large_err` in consumers that return `Result<_, ApiError>`.

## [0.1.1] - 2026-09-01

### Added

- `ApiError::with_header` and `ApiError::with_retry_after` add response headers while preserving the
  standard error envelope.

## [0.1.0] - 2026-08-25

### Added

- First public release of `baukit-http`.