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
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>(())
[pagination] adds keyset pagination whose opaque cursors are bound to the
request filters, and [classify_http_status] turns an upstream response into
a [RetryClass] so outbound clients share one retry policy.
Paginated handler
use axum::extract::Query;
use baukit_http::{ApiError, Page, PageKey, PageParams, 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)]
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)?;
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].