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}