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::CratestackError;
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/// [`CratestackError`] raised inside the dispatcher (e.g. body decode
17/// failure, unknown op id). The HTTP status comes from
18/// [`CratestackError::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: &CratestackError,
25) -> axum::response::Response
26where
27    C: HttpTransport,
28{
29    let body = RpcErrorBody::from_cratestack(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::CratestackErrorResponse`] (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(
56        response.into_body(),
57        cratestack_core::MAX_RESPONSE_REBUFFER_BYTES,
58    )
59    .await
60    {
61        Ok(bytes) => bytes.to_vec(),
62        Err(error) => {
63            // Buffering failed — synthesize an internal error frame.
64            let cool = CratestackError::Internal(format!("buffer handler error body: {error}"));
65            return encode_rpc_error(codec, headers, &cool);
66        }
67    };
68
69    let rpc_body = match decode_rpc_body::<_, cratestack_core::CratestackErrorResponse>(
70        codec,
71        headers,
72        &body_bytes,
73    ) {
74        Ok(parsed) => RpcErrorBody::from_cratestack_response(parsed),
75        Err(_) => {
76            // Handler emitted a non-2xx with a body that isn't the
77            // framework's REST error shape (unusual — would happen if a
78            // handler escaped through `into_response()` directly). Build
79            // a synthetic body from the status alone.
80            let cool = synthesize_error_for_status(status);
81            RpcErrorBody::from_cratestack(&cool)
82        }
83    };
84
85    encode_rpc_value_response(codec, headers, status, rpc_body)
86}
87
88fn encode_rpc_value_response<C, T>(
89    codec: &C,
90    headers: &HeaderMap,
91    status: axum::http::StatusCode,
92    value: T,
93) -> axum::response::Response
94where
95    C: HttpTransport,
96    T: Serialize,
97{
98    // Re-use the existing transport encoder so content negotiation
99    // happens via the same path as everything else.
100    crate::encode_transport_result_with_status_for::<_, T>(
101        codec,
102        headers,
103        &RPC_BINDING_CAPABILITIES,
104        status,
105        Ok(value),
106    )
107}