Skip to main content

cratestack_axum/rpc/
batch.rs

1//! Per-frame conversion for the batch path.
2
3use axum::http::HeaderMap;
4use cratestack_core::CratestackError;
5use cratestack_core::rpc::{RpcErrorBody, RpcResponseFrame};
6
7use crate::HttpTransport;
8
9use super::codec_helpers::decode_rpc_body;
10use super::util::synthesize_error_for_status;
11
12/// Convert an [`axum::Response`] returned by an inner dispatch arm into a
13/// single batch response frame.
14///
15/// Success bodies (2xx) are decoded as `serde_json::Value` via the same
16/// codec the request used and become `RpcResponseFrame::ok`. Error
17/// bodies (4xx/5xx) — which have already been post-processed by
18/// [`super::convert_handler_error_response`] inside `rpc_dispatch_inner` —
19/// are decoded as [`RpcErrorBody`] and inlined into
20/// `RpcResponseFrame::error` directly.
21///
22/// Wire limitation: success outputs must be representable as
23/// `serde_json::Value`. For CRUD/procedure outputs this is fine; if a
24/// future op returns CBOR-only types (e.g. raw byte strings without a
25/// JSON representation) the frame becomes an `internal` error.
26pub async fn response_to_frame<C>(
27    id: u64,
28    response: axum::response::Response,
29    codec: &C,
30    headers: &HeaderMap,
31) -> RpcResponseFrame
32where
33    C: HttpTransport,
34{
35    let status = response.status();
36    let body_bytes = match axum::body::to_bytes(
37        response.into_body(),
38        cratestack_core::MAX_RESPONSE_REBUFFER_BYTES,
39    )
40    .await
41    {
42        Ok(bytes) => bytes.to_vec(),
43        Err(error) => {
44            return RpcResponseFrame::err(
45                id,
46                &CratestackError::Internal(format!("buffer batch frame body: {error}")),
47            );
48        }
49    };
50
51    if status.is_success() {
52        match decode_rpc_body::<_, serde_json::Value>(codec, headers, &body_bytes) {
53            Ok(value) => RpcResponseFrame::ok(id, value),
54            Err(error) => RpcResponseFrame::err(id, &error),
55        }
56    } else {
57        // Body is already RpcErrorBody-shaped — `rpc_dispatch_inner`
58        // post-processes handler errors before they reach us.
59        match decode_rpc_body::<_, RpcErrorBody>(codec, headers, &body_bytes) {
60            Ok(body) => RpcResponseFrame {
61                id,
62                output: None,
63                error: Some(body),
64            },
65            Err(_) => {
66                // Defensive: a handler/dispatcher returned a non-2xx
67                // body that isn't RpcErrorBody-shaped. Synthesize one
68                // from the status alone rather than corrupting the
69                // batch envelope.
70                let synthetic = synthesize_error_for_status(status);
71                RpcResponseFrame::err(id, &synthetic)
72            }
73        }
74    }
75}