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//! [`pagination`] adds keyset pagination whose opaque cursors are bound to the
28//! request filters, and [`classify_http_status`] turns an upstream response into
29//! a [`RetryClass`] so outbound clients share one retry policy.
30//!
31//! # Paginated handler
32//!
33//! ```rust
34//! use axum::extract::Query;
35//! use baukit_http::{ApiError, Page, PageKey, PageParams, ResponseEnvelope};
36//! use serde::{Deserialize, Serialize};
37//! use uuid::Uuid;
38//!
39//! #[derive(Deserialize)]
40//! struct ListQuery {
41//!     limit: Option<i64>,
42//!     cursor: Option<String>,
43//!     category: Option<String>,
44//! }
45//!
46//! #[derive(Serialize)]
47//! struct Filters {
48//!     category: Option<String>,
49//! }
50//!
51//! #[derive(Clone, Serialize)]
52//! struct Item {
53//!     id: Uuid,
54//!     name: String,
55//! }
56//!
57//! #[derive(Serialize)]
58//! struct PageMeta {
59//!     next_cursor: Option<String>,
60//! }
61//!
62//! async fn list(
63//!     Query(query): Query<ListQuery>,
64//! ) -> Result<ResponseEnvelope<Vec<Item>, PageMeta>, ApiError> {
65//!     let params = PageParams::new(query.limit, query.cursor)?;
66//!     let filters = Filters { category: query.category };
67//!     let after = params.decode_cursor(&filters)?;
68//!
69//!     // Fetch `params.fetch_limit()?` rows ordered by (name, id), starting
70//!     // after `after.page_key::<String>()?` when it is present.
71//!     let _ = after;
72//!     let rows: Vec<Item> = Vec::new();
73//!
74//!     let page = Page::from_rows(rows, &params, &filters, |item| {
75//!         PageKey::new(item.name.clone(), item.id)
76//!     })?;
77//!     Ok(ResponseEnvelope::new(
78//!         page.items,
79//!         PageMeta { next_cursor: page.next_cursor },
80//!     ))
81//! }
82//! # let _ = list;
83//! ```
84//!
85//! This crate records HTTP metrics through the [`metrics`] facade and never
86//! installs a recorder. The recorder owner must configure
87//! `http_request_duration_seconds` with [`DURATION_BUCKETS`].
88
89#![deny(missing_docs)]
90
91mod error;
92mod extract;
93mod locale;
94mod middleware;
95mod options;
96pub mod pagination;
97pub mod retry;
98mod routing;
99
100pub use baukit_openapi::{ErrorBody, ErrorEnvelope, ResponseEnvelope, Rfc3339DateTime};
101pub use error::ApiError;
102pub use extract::{ApiJson, ApiPath, ApiQuery, Path, Query};
103pub use locale::{
104    LocaleQueryOverride, MAX_ACCEPT_LANGUAGE_BYTES, MAX_LOCALE_QUERY_BYTES, MAX_SUPPORTED_LOCALES,
105    RequestLocale, RequestLocaleConfig, RequestLocaleConfigError, RequestLocaleRejection,
106};
107pub use middleware::{
108    DURATION_BUCKETS, HTTP_REQUEST_DURATION_SECONDS, HTTP_REQUESTS_IN_FLIGHT, HTTP_REQUESTS_TOTAL,
109    RequestId, X_REQUEST_ID, extract_trace_context, inject_current_trace_context,
110    inject_trace_context, layers,
111};
112pub use options::{HttpOptions, HttpOptionsError, JsonRejectionCodes};
113pub use pagination::{
114    Cursor, DEFAULT_PAGE_LIMIT, MAX_PAGE_LIMIT, Page, PageKey, PageParams, PaginationError,
115};
116pub use retry::{
117    RetryClass, RetryHeaderOptions, classify_http_status, classify_http_status_with_options,
118    classify_transport_error, retry_after_from_headers, retry_after_from_headers_at,
119    retry_after_from_headers_with_options, retry_after_from_headers_with_options_at,
120};
121pub use routing::finalize;
122
123/// The metrics facade used by the HTTP lifecycle middleware.
124///
125/// Applications normally initialize a recorder through `baukit-telemetry`.
126pub use metrics;
127
128#[cfg(test)]
129mod tests;
130
131// Compiles the README's examples so they cannot drift from the API.
132#[doc = include_str!("../README.md")]
133#[cfg(doctest)]
134struct ReadmeDoctests;