Skip to main content

cratestack_axum/rpc/
error_encode.rs

1//! Wire-side error encoding: dispatcher errors and handler-emitted error
2//! responses both end up as [`RpcErrorBody`] frames.
3
4use axum::http::HeaderMap;
5use cratestack_core::CoolError;
6use cratestack_core::rpc::RpcErrorBody;
7use serde::Serialize;
8
9use crate::HttpTransport;
10
11use super::RPC_BINDING_CAPABILITIES;
12use super::codec_helpers::decode_rpc_body;
13use super::util::synthesize_error_for_status;
14
15/// Build an `axum::Response` carrying an [`RpcErrorBody`] for a
16/// [`CoolError`] raised inside the dispatcher (e.g. body decode
17/// failure, unknown op id). The HTTP status comes from
18/// [`CoolError::status_code`]; the body is codec-encoded via the
19/// request's codec, content-type negotiated against
20/// [`RPC_BINDING_CAPABILITIES`].
21pub fn encode_rpc_error<C>(
22    codec: &C,
23    headers: &HeaderMap,
24    error: &CoolError,
25) -> axum::response::Response
26where
27    C: HttpTransport,
28{
29    let body = RpcErrorBody::from_cool(error);
30    let status = error.status_code();
31    encode_rpc_value_response(codec, headers, status, body)
32}
33
34/// Post-process a handler-emitted response. Success responses pass
35/// through unchanged. Non-2xx responses are buffered, their bodies
36/// decoded as [`cratestack_core::CoolErrorResponse`] (the REST shape
37/// the existing axum handlers emit), translated to [`RpcErrorBody`]
38/// with the gRPC-style code, and re-encoded with the same HTTP status.
39///
40/// Called once per dispatch (inside `rpc_dispatch_inner`) so unary and
41/// batch both see uniformly RpcErrorBody-shaped error bodies.
42pub async fn convert_handler_error_response<C>(
43    response: axum::response::Response,
44    codec: &C,
45    headers: &HeaderMap,
46) -> axum::response::Response
47where
48    C: HttpTransport,
49{
50    if response.status().is_success() {
51        return response;
52    }
53
54    let status = response.status();
55    let body_bytes = match axum::body::to_bytes(response.into_body(), usize::MAX).await {
56        Ok(bytes) => bytes.to_vec(),
57        Err(error) => {
58            // Buffering failed — synthesize an internal error frame.
59            let cool = CoolError::Internal(format!("buffer handler error body: {error}"));
60            return encode_rpc_error(codec, headers, &cool);
61        }
62    };
63
64    let rpc_body =
65        match decode_rpc_body::<_, cratestack_core::CoolErrorResponse>(codec, headers, &body_bytes)
66        {
67            Ok(parsed) => RpcErrorBody::from_cool_response(parsed),
68            Err(_) => {
69                // Handler emitted a non-2xx with a body that isn't the
70                // framework's REST error shape (unusual — would happen if a
71                // handler escaped through `into_response()` directly). Build
72                // a synthetic body from the status alone.
73                let cool = synthesize_error_for_status(status);
74                RpcErrorBody::from_cool(&cool)
75            }
76        };
77
78    encode_rpc_value_response(codec, headers, status, rpc_body)
79}
80
81fn encode_rpc_value_response<C, T>(
82    codec: &C,
83    headers: &HeaderMap,
84    status: axum::http::StatusCode,
85    value: T,
86) -> axum::response::Response
87where
88    C: HttpTransport,
89    T: Serialize,
90{
91    // Re-use the existing transport encoder so content negotiation
92    // happens via the same path as everything else.
93    crate::encode_transport_result_with_status_for::<_, T>(
94        codec,
95        headers,
96        &RPC_BINDING_CAPABILITIES,
97        status,
98        Ok(value),
99    )
100}