Skip to main content

baukit_http/
lib.rs

1//! Shared Axum middleware, HTTP telemetry, and public errors for Baukit services.
2//!
3//! [`finalize`] applies one consistent request lifecycle: standard extractor and
4//! routing errors, request identity, W3C trace extraction and response
5//! propagation, route-template spans, HTTP RED metrics, panic and timeout
6//! envelopes, body and concurrency limits, and explicit CORS.
7//!
8//! # Example
9//!
10//! ```rust
11//! use axum::{Router, routing::get};
12//! use baukit_http::{HttpOptions, RequestId, finalize};
13//!
14//! async fn hello(request_id: RequestId) -> String {
15//!     format!("hello from request {}", request_id.as_str())
16//! }
17//!
18//! let options = HttpOptions::default()
19//!     .with_allowed_origins(["https://app.example.com"])?
20//!     .with_additional_allowed_headers(["accept", "x-webhook-secret"])?
21//!     .with_json_rejection_code("invalid_json")?;
22//! let app = finalize(Router::new().route("/hello", get(hello)), options);
23//! # let _: Router = app;
24//! # Ok::<(), baukit_http::HttpOptionsError>(())
25//! ```
26//!
27//! [`classify_http_status`] turns an upstream response into a [`RetryClass`] so
28//! outbound clients share one retry policy.
29//!
30//! # Paginated handler
31//!
32//! Keyset pagination lives in `baukit_core::pagination` so domain crates can
33//! use it without Axum. `PaginationError` converts into [`ApiError`].
34//!
35//! ```rust
36//! use axum::extract::Query;
37//! use baukit_core::pagination::{Page, PageKey, PageParams};
38//! use baukit_http::{ApiError, ResponseEnvelope};
39//! use serde::{Deserialize, Serialize};
40//! use uuid::Uuid;
41//!
42//! #[derive(Deserialize)]
43//! struct ListQuery {
44//!     limit: Option<i64>,
45//!     cursor: Option<String>,
46//!     category: Option<String>,
47//! }
48//!
49//! #[derive(Serialize)]
50//! struct Filters {
51//!     category: Option<String>,
52//! }
53//!
54//! #[derive(Clone, Serialize)]
55//! struct Item {
56//!     id: Uuid,
57//!     name: String,
58//! }
59//!
60//! #[derive(Serialize)]
61//! #[serde(rename_all = "camelCase")]
62//! struct PageMeta {
63//!     next_cursor: Option<String>,
64//! }
65//!
66//! async fn list(
67//!     Query(query): Query<ListQuery>,
68//! ) -> Result<ResponseEnvelope<Vec<Item>, PageMeta>, ApiError> {
69//!     let params = PageParams::new(query.limit, query.cursor)?;
70//!     let filters = Filters { category: query.category };
71//!     let after = params.decode_cursor(&filters)?;
72//!
73//!     // Fetch `params.fetch_limit()?` rows ordered by (name, id), starting
74//!     // after `after.page_key::<String>()?` when it is present.
75//!     let _ = after;
76//!     let rows: Vec<Item> = Vec::new();
77//!
78//!     let page = Page::from_rows(rows, &params, &filters, |item| {
79//!         PageKey::new(item.name.clone(), item.id)
80//!     })?;
81//!     Ok(ResponseEnvelope::new(
82//!         page.items,
83//!         PageMeta { next_cursor: page.next_cursor },
84//!     ))
85//! }
86//! # let _ = list;
87//! ```
88//!
89//! This crate records HTTP metrics through the [`metrics`] facade and never
90//! installs a recorder. The recorder owner must configure
91//! `http_request_duration_seconds` with [`DURATION_BUCKETS`].
92
93#![deny(missing_docs)]
94
95mod error;
96mod extract;
97mod idempotency;
98mod locale;
99mod middleware;
100mod options;
101mod precondition;
102pub mod retry;
103mod routing;
104
105pub use baukit_openapi::{ErrorBody, ErrorEnvelope, ResponseEnvelope, Rfc3339DateTime};
106pub use error::ApiError;
107pub use extract::{ApiJson, ApiPath, ApiQuery, Path, Query};
108pub use idempotency::{
109    IDEMPOTENCY_KEY, IDEMPOTENCY_KEY_IN_PROGRESS_CODE, IDEMPOTENCY_KEY_REQUIRED_CODE,
110    IDEMPOTENCY_KEY_REUSED_CODE, INVALID_IDEMPOTENCY_KEY_CODE, IdempotencyError, IdempotencyKey,
111    IdempotencyKeyRule, InvalidIdempotencyKey, InvalidIdempotencyKeyRule,
112    MAX_IDEMPOTENCY_KEY_BYTES,
113};
114pub use locale::{
115    LocaleQueryOverride, MAX_ACCEPT_LANGUAGE_BYTES, MAX_LOCALE_QUERY_BYTES, MAX_SUPPORTED_LOCALES,
116    RequestLocale, RequestLocaleConfig, RequestLocaleConfigError, RequestLocaleRejection,
117};
118pub use middleware::{
119    DURATION_BUCKETS, HTTP_REQUEST_DURATION_SECONDS, HTTP_REQUESTS_IN_FLIGHT, HTTP_REQUESTS_TOTAL,
120    RequestId, X_REQUEST_ID, extract_trace_context, inject_current_trace_context,
121    inject_trace_context, layers,
122};
123pub use options::{HttpOptions, HttpOptionsError, JsonRejectionCodes, ResponseCachePolicy};
124pub use precondition::{
125    InvalidEtagPrefix, InvalidIfMatch, MAX_ETAG_PREFIX_BYTES, PreconditionError, Revision,
126    RevisionEtag, RevisionOutOfRange, ensure_current_revision,
127};
128pub use retry::{
129    RetryClass, RetryHeaderOptions, classify_http_status, classify_http_status_with_options,
130    classify_http_status_with_options_at, classify_transport_error, retry_after_from_headers,
131    retry_after_from_headers_at, retry_after_from_headers_with_options,
132    retry_after_from_headers_with_options_at,
133};
134pub use routing::finalize;
135
136/// The metrics facade used by the HTTP lifecycle middleware.
137///
138/// Applications normally initialize a recorder through `baukit-telemetry`.
139pub use metrics;
140
141#[cfg(test)]
142mod tests;
143
144// Compiles the README's examples so they cannot drift from the API.
145#[doc = include_str!("../README.md")]
146#[cfg(doctest)]
147struct ReadmeDoctests;