soma_http_server/rejection.rs
1//! Generic rejection envelopes for routing-level failures: unmatched
2//! routes and disallowed methods. Rendered through `soma_http_api`'s
3//! structured error body so any HTTP surface that mounts these renders the
4//! same shape for the same failure class. Body-extraction rejections
5//! (malformed JSON, payload too large) are
6//! `soma_http_api::response::json_rejection_response` — an API-shape
7//! concern, not a transport concern, so they live there instead.
8//!
9//! [`not_found_handler`] is wired into `apps/soma`'s router `.fallback()`.
10//! [`method_not_allowed`] is available for a product router to wire up (via
11//! Axum's per-route method fallback) but is not currently mounted anywhere
12//! — Axum's `.fallback()` only intercepts unmatched *paths*, not
13//! matched-path/disallowed-method requests, so a consumer must opt in
14//! separately for 405s to use this envelope.
15
16use axum::{http::StatusCode, response::Response};
17use soma_http_api::problem::ErrorBody;
18
19/// Render a `404 Not Found` body: `{"error": "not_found"}`.
20pub fn not_found() -> Response {
21 ErrorBody::new("not_found").into_response_with_status(StatusCode::NOT_FOUND)
22}
23
24/// Axum `.fallback()` handler wrapping [`not_found`]. Mount with
25/// `router.fallback(not_found_handler)`.
26pub async fn not_found_handler() -> Response {
27 not_found()
28}
29
30/// Render a `405 Method Not Allowed` body:
31/// `{"error": "method_not_allowed"}`.
32pub fn method_not_allowed() -> Response {
33 ErrorBody::new("method_not_allowed").into_response_with_status(StatusCode::METHOD_NOT_ALLOWED)
34}
35
36#[cfg(test)]
37mod tests {
38 use super::*;
39
40 #[test]
41 fn not_found_is_404_with_stable_error_code() {
42 let response = not_found();
43 assert_eq!(response.status(), StatusCode::NOT_FOUND);
44 }
45
46 #[test]
47 fn method_not_allowed_is_405_with_stable_error_code() {
48 let response = method_not_allowed();
49 assert_eq!(response.status(), StatusCode::METHOD_NOT_ALLOWED);
50 }
51
52 #[tokio::test]
53 async fn not_found_handler_matches_not_found_body() {
54 use axum::body::to_bytes;
55
56 let direct = to_bytes(not_found().into_body(), usize::MAX).await.unwrap();
57 let via_handler = to_bytes(not_found_handler().await.into_body(), usize::MAX)
58 .await
59 .unwrap();
60 assert_eq!(direct, via_handler);
61 }
62}