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, ¶ms, &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 middleware;
94mod options;
95pub mod pagination;
96pub mod retry;
97mod routing;
98
99pub use baukit_openapi::{ErrorBody, ErrorEnvelope, ResponseEnvelope, Rfc3339DateTime};
100pub use error::ApiError;
101pub use extract::{ApiJson, ApiPath, ApiQuery, Path, Query};
102pub use middleware::{
103 DURATION_BUCKETS, HTTP_REQUEST_DURATION_SECONDS, HTTP_REQUESTS_IN_FLIGHT, HTTP_REQUESTS_TOTAL,
104 RequestId, X_REQUEST_ID, extract_trace_context, inject_current_trace_context,
105 inject_trace_context, layers,
106};
107pub use options::{HttpOptions, HttpOptionsError};
108pub use pagination::{
109 Cursor, DEFAULT_PAGE_LIMIT, MAX_PAGE_LIMIT, Page, PageKey, PageParams, PaginationError,
110};
111pub use retry::{
112 RetryClass, RetryHeaderOptions, classify_http_status, classify_http_status_with_options,
113 classify_transport_error, retry_after_from_headers, retry_after_from_headers_at,
114 retry_after_from_headers_with_options, retry_after_from_headers_with_options_at,
115};
116pub use routing::finalize;
117
118/// The metrics facade used by the HTTP lifecycle middleware.
119///
120/// Applications normally initialize a recorder through `baukit-telemetry`.
121pub use metrics;
122
123#[cfg(test)]
124mod tests;
125
126// Compiles the README's examples so they cannot drift from the API.
127#[doc = include_str!("../README.md")]
128#[cfg(doctest)]
129struct ReadmeDoctests;