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