Skip to main content

netget/server/connect_rpc/
wire.rs

1//! Bounded Connect protocol boundary; only protobuf messages enter the shared typed codec.
2pub use crate::server::grpc::http1::bounded_headers;
3use bytes::{Buf, Bytes};
4use http::{HeaderMap, HeaderName, HeaderValue, Request, Response, StatusCode};
5use http_body_util::{BodyExt, Full, Limited, StreamBody};
6use hyper::body::Frame;
7use serde::Deserialize;
8use serde_json::{json, Value};
9use std::{
10    collections::BTreeMap,
11    io::Read,
12    sync::{
13        atomic::{AtomicBool, Ordering},
14        Arc, Mutex,
15    },
16};
17use tonic::{body::BoxBody, Code, Status};
18pub const MAX_MESSAGE_BYTES: usize = crate::server::grpc::stream_codec::MAX_MESSAGE_BYTES;
19pub const MAX_END_BYTES: usize = 16 * 1024;
20pub const MAX_MESSAGES: usize = 256;
21#[derive(Clone, Copy, Debug, PartialEq, Eq)]
22pub enum Shape {
23    Unary,
24    Stream,
25}
26impl Shape {
27    pub fn content_type(self) -> &'static str {
28        match self {
29            Self::Unary => "application/proto",
30            Self::Stream => "application/connect+proto",
31        }
32    }
33}
34pub fn shape(headers: &HeaderMap) -> Result<Shape, Status> {
35    match unique(headers, "content-type")? {
36        Some("application/proto") => Ok(Shape::Unary),
37        Some("application/connect+proto") => Ok(Shape::Stream),
38        _ => Err(Status::unimplemented(
39            "binary protobuf Connect content type required",
40        )),
41    }
42}
43pub fn unique<'a>(headers: &'a HeaderMap, name: &str) -> Result<Option<&'a str>, Status> {
44    let mut values = headers.get_all(name).iter();
45    let value = values.next();
46    if values.next().is_some() {
47        return Err(Status::invalid_argument("duplicate reserved header"));
48    }
49    value
50        .map(|value| {
51            value
52                .to_str()
53                .map_err(|_| Status::invalid_argument("invalid reserved header"))
54        })
55        .transpose()
56}
57pub fn encoding(headers: &HeaderMap, name: &str) -> Result<bool, Status> {
58    match unique(headers, name)? {
59        None | Some("identity") => Ok(false),
60        Some("gzip") => Ok(true),
61        _ => Err(Status::unimplemented("supported encodings: identity, gzip")),
62    }
63}
64pub fn accepts_gzip(headers: &HeaderMap, name: &str, fallback: bool) -> Result<bool, Status> {
65    let Some(value) = unique(headers, name)? else {
66        return Ok(fallback);
67    };
68    if value.len() > 1024 || value.split(',').count() > 16 {
69        return Err(Status::invalid_argument("encoding list exceeds bound"));
70    }
71    Ok(value.split(',').any(|value| value.trim() == "gzip"))
72}
73pub fn timeout_ms(headers: &HeaderMap) -> Result<Option<u64>, Status> {
74    let Some(value) = unique(headers, "connect-timeout-ms")? else {
75        return Ok(None);
76    };
77    if value.is_empty() || value.len() > 10 || !value.bytes().all(|byte| byte.is_ascii_digit()) {
78        return Err(Status::invalid_argument(
79            "connect-timeout-ms must have 1..10 decimal digits",
80        ));
81    }
82    let n = value
83        .parse::<u64>()
84        .map_err(|_| Status::invalid_argument("invalid Connect timeout"))?;
85    if n == 0 {
86        return Err(Status::invalid_argument("Connect timeout must be positive"));
87    }
88    Ok(Some(n))
89}
90pub fn inflate(input: &[u8], maximum: usize) -> Result<Vec<u8>, Status> {
91    if input.is_empty() {
92        return Ok(Vec::new());
93    }
94    let mut output = Vec::new();
95    flate2::bufread::MultiGzDecoder::new(input)
96        .take((maximum + 1) as u64)
97        .read_to_end(&mut output)
98        .map_err(|_| Status::invalid_argument("invalid gzip member or checksum"))?;
99    if output.len() > maximum {
100        return Err(Status::resource_exhausted("expanded payload exceeds bound"));
101    }
102    Ok(output)
103}
104pub fn frame(flag: u8, payload: &[u8]) -> Bytes {
105    let mut bytes = Vec::with_capacity(payload.len() + 5);
106    bytes.push(flag);
107    bytes.extend((payload.len() as u32).to_be_bytes());
108    bytes.extend(payload);
109    bytes.into()
110}
111pub fn body(bytes: Bytes) -> BoxBody {
112    Full::new(bytes)
113        .map_err(|never| match never {})
114        .boxed_unsync()
115}
116pub async fn collect(body: BoxBody, maximum: usize) -> Result<Bytes, Status> {
117    Limited::new(request_body(body), maximum + 1)
118        .collect()
119        .await
120        .map_err(|_| Status::resource_exhausted("body exceeds bound or has HTTP trailers"))
121        .map(|collected| collected.to_bytes())
122}
123pub fn request_body(body: BoxBody) -> BoxBody {
124    StreamBody::new(futures::stream::try_unfold(body, |mut body| async move {
125        let Some(frame) = body.frame().await else {
126            return Ok(None);
127        };
128        let frame = frame?;
129        if !frame.is_data() {
130            return Err(Status::invalid_argument("Connect forbids HTTP trailers"));
131        }
132        Ok(Some((frame, body)))
133    }))
134    .boxed_unsync()
135}
136pub fn unary_request(bytes: &[u8], compressed: bool) -> Result<Bytes, Status> {
137    if bytes.len() > MAX_MESSAGE_BYTES {
138        return Err(Status::resource_exhausted("request exceeds 4 MiB"));
139    }
140    let expanded;
141    let bytes = if compressed {
142        expanded = inflate(bytes, MAX_MESSAGE_BYTES)?;
143        expanded.as_slice()
144    } else {
145        bytes
146    };
147    Ok(frame(0, bytes))
148}
149pub fn streaming_request(bytes: &[u8], gzip: bool) -> Result<Bytes, Status> {
150    if bytes.len() < 5 {
151        return Err(Status::invalid_argument(
152            "one complete request envelope required",
153        ));
154    }
155    let length = u32::from_be_bytes(bytes[1..5].try_into().unwrap()) as usize;
156    if length > MAX_MESSAGE_BYTES || bytes.len() > MAX_MESSAGE_BYTES + 5 {
157        return Err(Status::resource_exhausted("request exceeds 4 MiB"));
158    }
159    if !matches!(bytes[0], 0 | 1) || (bytes[0] == 1 && !gzip) || length != bytes.len() - 5 {
160        return Err(Status::invalid_argument(
161            "one protobuf request envelope and EOF required",
162        ));
163    }
164    // Expanded size/checksum is independently validated before the native decoder.
165    if bytes[0] == 1 {
166        inflate(&bytes[5..], MAX_MESSAGE_BYTES)?;
167    }
168    Ok(Bytes::copy_from_slice(bytes))
169}
170pub fn code_name(code: Code) -> &'static str {
171    match code {
172        Code::Ok => "ok",
173        Code::Cancelled => "canceled",
174        Code::Unknown => "unknown",
175        Code::InvalidArgument => "invalid_argument",
176        Code::DeadlineExceeded => "deadline_exceeded",
177        Code::NotFound => "not_found",
178        Code::AlreadyExists => "already_exists",
179        Code::PermissionDenied => "permission_denied",
180        Code::ResourceExhausted => "resource_exhausted",
181        Code::FailedPrecondition => "failed_precondition",
182        Code::Aborted => "aborted",
183        Code::OutOfRange => "out_of_range",
184        Code::Unimplemented => "unimplemented",
185        Code::Internal => "internal",
186        Code::Unavailable => "unavailable",
187        Code::DataLoss => "data_loss",
188        Code::Unauthenticated => "unauthenticated",
189    }
190}
191fn named_code(name: &str) -> Option<Code> {
192    (1..=16)
193        .map(Code::from_i32)
194        .find(|code| code_name(*code) == name)
195}
196pub fn http_status(code: Code) -> StatusCode {
197    StatusCode::from_u16(match code {
198        Code::Ok => 200,
199        Code::Cancelled => 499,
200        Code::InvalidArgument | Code::FailedPrecondition | Code::OutOfRange => 400,
201        Code::DeadlineExceeded => 504,
202        Code::NotFound => 404,
203        Code::AlreadyExists | Code::Aborted => 409,
204        Code::PermissionDenied => 403,
205        Code::ResourceExhausted => 429,
206        Code::Unimplemented => 501,
207        Code::Unavailable => 503,
208        Code::Unauthenticated => 401,
209        _ => 500,
210    })
211    .unwrap()
212}
213pub fn inferred_code(status: StatusCode) -> Code {
214    match status.as_u16() {
215        400 => Code::Internal,
216        401 => Code::Unauthenticated,
217        403 => Code::PermissionDenied,
218        404 => Code::Unimplemented,
219        429 | 502 | 503 | 504 => Code::Unavailable,
220        _ => Code::Unknown,
221    }
222}
223pub fn error_json(status: &Status) -> Value {
224    json!({"code":code_name(status.code()),"message":crate::utils::truncate_for_llm(status.message(),4096)})
225}
226#[derive(Deserialize)]
227struct Error {
228    code: String,
229    #[serde(default)]
230    message: String,
231    #[serde(default)]
232    details: Vec<Value>,
233}
234impl Error {
235    fn status(self) -> Result<Status, Status> {
236        let code =
237            named_code(&self.code).ok_or_else(|| Status::internal("invalid Connect error code"))?;
238        if self.message.len() > 4096 || !self.details.is_empty() {
239            return Err(Status::internal(
240                "error message bound or unsupported binary details",
241            ));
242        }
243        Ok(Status::new(code, self.message))
244    }
245}
246#[derive(Default, Clone, Debug)]
247pub struct Metadata(pub BTreeMap<String, Vec<String>>);
248impl<'de> Deserialize<'de> for Metadata {
249    fn deserialize<D: serde::Deserializer<'de>>(deserializer: D) -> Result<Self, D::Error> {
250        struct Visitor;
251        impl<'de> serde::de::Visitor<'de> for Visitor {
252            type Value = Metadata;
253            fn expecting(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result {
254                f.write_str("bounded ASCII metadata arrays")
255            }
256            fn visit_map<A: serde::de::MapAccess<'de>>(
257                self,
258                mut map: A,
259            ) -> Result<Metadata, A::Error> {
260                let mut result = Metadata::default();
261                let mut total = 0usize;
262                let mut values = 0usize;
263                while let Some(key) = map.next_key::<String>()? {
264                    let key = key.to_ascii_lowercase();
265                    if result.0.len() >= 16 || !metadata_key(&key) || result.0.contains_key(&key) {
266                        return Err(serde::de::Error::custom(
267                            "metadata name/count/duplicate bound",
268                        ));
269                    }
270                    let entries = map.next_value::<Vec<String>>()?;
271                    if entries.is_empty() {
272                        return Err(serde::de::Error::custom("metadata needs a value"));
273                    }
274                    for value in &entries {
275                        values += 1;
276                        total = total.saturating_add(key.len() + value.len());
277                        if values > 32 || total > 8192 || !metadata_value(value) {
278                            return Err(serde::de::Error::custom("metadata value/total bound"));
279                        }
280                    }
281                    result.0.insert(key, entries);
282                }
283                Ok(result)
284            }
285        }
286        deserializer.deserialize_map(Visitor)
287    }
288}
289fn metadata_key(key: &str) -> bool {
290    !key.is_empty()
291        && key.len() <= 128
292        && key.bytes().all(|byte| {
293            byte.is_ascii_lowercase() || byte.is_ascii_digit() || matches!(byte, b'_' | b'-' | b'.')
294        })
295        && !key.ends_with("-bin")
296        && !key.starts_with("connect-")
297        && !key.starts_with("grpc-")
298        && !key.starts_with("trailer-")
299        && !matches!(
300            key,
301            "host"
302                | "te"
303                | "content-type"
304                | "content-length"
305                | "transfer-encoding"
306                | "content-encoding"
307                | "accept-encoding"
308                | "connection"
309                | "origin"
310                | "accept"
311                | "user-agent"
312                | "x-user-agent"
313                | "x-grpc-web"
314        )
315}
316fn metadata_value(value: &str) -> bool {
317    !value.is_empty() && value.len() <= 1024 && value.bytes().all(|byte| (32..=126).contains(&byte))
318}
319impl Metadata {
320    pub fn json(&self) -> Value {
321        json!(self.0)
322    }
323    pub fn from_action(value: &Value) -> Result<Self, Status> {
324        let object = value
325            .as_object()
326            .ok_or_else(|| Status::invalid_argument("metadata must be an object"))?;
327        let arrays: serde_json::Map<String, Value> = object
328            .iter()
329            .map(|(key, value)| (key.clone(), json!([value])))
330            .collect();
331        serde_json::from_value(Value::Object(arrays))
332            .map_err(|_| Status::invalid_argument("metadata exceeds selected ASCII bounds"))
333    }
334    pub fn append_headers(&self, headers: &mut HeaderMap, prefix: &str) -> Result<(), Status> {
335        for (key, values) in &self.0 {
336            let name: HeaderName = format!("{prefix}{key}")
337                .parse()
338                .map_err(|_| Status::internal("invalid metadata name"))?;
339            for value in values {
340                headers.append(
341                    name.clone(),
342                    HeaderValue::from_str(value)
343                        .map_err(|_| Status::internal("invalid metadata value"))?,
344                );
345            }
346        }
347        Ok(())
348    }
349    pub fn from_headers(headers: &HeaderMap, prefix: &str) -> Result<Self, Status> {
350        let mut value = serde_json::Map::new();
351        for key in headers.keys() {
352            let name = key.as_str();
353            let selected = if prefix.is_empty() {
354                if metadata_key(name)
355                    && !name.starts_with("access-control-")
356                    && !matches!(
357                        name,
358                        "date" | "server" | "vary" | "retry-after" | "keep-alive"
359                    )
360                {
361                    Some(name)
362                } else {
363                    None
364                }
365            } else {
366                name.strip_prefix(prefix)
367            };
368            if let Some(name) = selected {
369                let values = headers
370                    .get_all(key)
371                    .iter()
372                    .map(|value| {
373                        value
374                            .to_str()
375                            .map(Value::from)
376                            .map_err(|_| Status::internal("non-ASCII metadata"))
377                    })
378                    .collect::<Result<Vec<_>, _>>()?;
379                value.insert(name.into(), Value::Array(values));
380            }
381        }
382        serde_json::from_value(Value::Object(value))
383            .map_err(|_| Status::resource_exhausted("response metadata exceeds bounds"))
384    }
385}
386fn present_error<'de, D: serde::Deserializer<'de>>(d: D) -> Result<Option<Error>, D::Error> {
387    Error::deserialize(d).map(Some)
388}
389#[derive(Deserialize)]
390struct EndStream {
391    #[serde(default, deserialize_with = "present_error")]
392    error: Option<Error>,
393    #[serde(default)]
394    metadata: Metadata,
395}
396pub fn end_stream(bytes: &[u8]) -> Result<HeaderMap, Status> {
397    if bytes.len() > MAX_END_BYTES {
398        return Err(Status::resource_exhausted("EndStream exceeds 16 KiB"));
399    }
400    let end: EndStream =
401        serde_json::from_slice(bytes).map_err(|_| Status::internal("invalid EndStream JSON"))?;
402    let status = match end.error {
403        Some(error) => error.status()?,
404        None => Status::ok(""),
405    };
406    let mut headers = HeaderMap::new();
407    end.metadata.append_headers(&mut headers, "")?;
408    status.add_header(&mut headers)?;
409    Ok(headers)
410}
411#[derive(Default)]
412pub struct ResponseMetadata {
413    pub leading: Metadata,
414    pub trailing: Metadata,
415    pub sealed: bool,
416}
417#[derive(Clone)]
418pub struct RpcMetadata {
419    pub request: Value,
420    pub response: Arc<Mutex<ResponseMetadata>>,
421}
422impl RpcMetadata {
423    pub fn new(headers: &HeaderMap) -> Result<Self, Status> {
424        Ok(Self {
425            request: Metadata::from_headers(headers, "")?.json(),
426            response: Arc::new(Mutex::new(ResponseMetadata::default())),
427        })
428    }
429    pub fn apply(&self, action: &Value) -> Result<(), Status> {
430        let value = Metadata::from_action(&action["metadata"])?;
431        let mut response = self.response.lock().unwrap_or_else(|e| e.into_inner());
432        match action["phase"].as_str() {
433            Some("headers") if !response.sealed => response.leading = value,
434            Some("trailers") => response.trailing = value,
435            _ => {
436                return Err(Status::failed_precondition(
437                    "leading metadata requires first response not yet emitted",
438                ))
439            }
440        }
441        Ok(())
442    }
443}
444pub fn metadata_action() -> crate::llm::actions::ActionDefinition {
445    use crate::llm::actions::{ActionDefinition, Parameter};
446    use crate::protocol::LogTemplate;
447    ActionDefinition {
448        name: "connect_rpc_metadata".into(),
449        description: "Replace bounded ASCII response metadata; headers before first message, trailers before finish".into(),
450        parameters: vec![
451            Parameter {
452                name: "phase".into(),
453                type_hint: "string".into(),
454                description: "Set headers before the first response message, or trailers before finishing the RPC".into(),
455                required: true,
456            },
457            Parameter {
458                name: "metadata".into(),
459                type_hint: "object".into(),
460                description: "At most 16 lowercase ASCII keys/string values, 8 KiB; no reserved or binary names".into(),
461                required: true,
462            },
463        ],
464        example: json!({"type":"connect_rpc_metadata","phase":"trailers","metadata":{"x-note":"done"}}),
465        log_template: Some(LogTemplate::new().with_info("ConnectRPC response metadata replaced")),
466    }
467}
468pub fn validate_metadata_action(action: &Value) -> anyhow::Result<()> {
469    anyhow::ensure!(
470        matches!(action["phase"].as_str(), Some("headers" | "trailers")),
471        "phase must be headers or trailers"
472    );
473    Metadata::from_action(&action["metadata"])?;
474    Ok(())
475}
476
477enum Item {
478    Data(u8, Bytes),
479    Trailers(HeaderMap),
480    Eof,
481}
482struct Reader {
483    body: BoxBody,
484    chunk: Bytes,
485    trailers: Option<HeaderMap>,
486    ended: bool,
487    native: bool,
488}
489impl Reader {
490    fn new(body: BoxBody, native: bool) -> Self {
491        Self {
492            body,
493            chunk: Bytes::new(),
494            trailers: None,
495            ended: false,
496            native,
497        }
498    }
499    async fn available(&mut self) -> Result<bool, Status> {
500        while self.chunk.is_empty() && !self.ended && self.trailers.is_none() {
501            match self.body.frame().await {
502                None => self.ended = true,
503                Some(Err(status)) => return Err(status),
504                Some(Ok(frame)) => match frame.into_data() {
505                    Ok(data) => self.chunk = data,
506                    Err(frame) => {
507                        if !self.native {
508                            return Err(Status::internal("Connect forbids HTTP trailers"));
509                        }
510                        self.trailers = Some(
511                            frame
512                                .into_trailers()
513                                .map_err(|_| Status::internal("invalid native frame"))?,
514                        );
515                    }
516                },
517            }
518        }
519        Ok(!self.chunk.is_empty())
520    }
521    async fn read(&mut self, n: usize, out: &mut Vec<u8>) -> Result<(), Status> {
522        let mut left = n;
523        while left > 0 {
524            if !self.available().await? {
525                return Err(Status::internal("truncated envelope"));
526            }
527            let n = left.min(self.chunk.len());
528            out.extend(&self.chunk[..n]);
529            self.chunk.advance(n);
530            left -= n;
531        }
532        Ok(())
533    }
534    async fn next(&mut self) -> Result<Item, Status> {
535        if !self.available().await? {
536            return Ok(match self.trailers.take() {
537                Some(headers) => Item::Trailers(headers),
538                None => Item::Eof,
539            });
540        }
541        let mut prefix = Vec::with_capacity(5);
542        self.read(5, &mut prefix).await?;
543        let flag = prefix[0];
544        if (self.native && !matches!(flag, 0 | 1)) || (!self.native && flag > 3) {
545            return Err(Status::internal("unsupported envelope flag"));
546        }
547        let length = u32::from_be_bytes(prefix[1..5].try_into().unwrap()) as usize;
548        let maximum = if !self.native && flag & 2 != 0 {
549            MAX_END_BYTES
550        } else {
551            MAX_MESSAGE_BYTES
552        };
553        if length > maximum {
554            return Err(Status::resource_exhausted("envelope prefix exceeds bound"));
555        }
556        let mut payload = Vec::with_capacity(length);
557        self.read(length, &mut payload).await?;
558        Ok(Item::Data(flag, payload.into()))
559    }
560}
561pub fn client_stream_body(body: BoxBody, gzip: bool, complete: Arc<AtomicBool>) -> BoxBody {
562    StreamBody::new(futures::stream::try_unfold(
563        (Reader::new(body, false), 0usize, false),
564        move |(mut reader, count, ended)| {
565            let complete = complete.clone();
566            async move {
567                if ended {
568                    return Ok(None);
569                }
570                let Item::Data(flag, payload) = reader.next().await? else {
571                    return Err(Status::internal("response lacks final EndStream"));
572                };
573                if flag & 1 != 0 && !gzip {
574                    return Err(Status::internal(
575                        "compression flag without gzip negotiation",
576                    ));
577                }
578                if flag & 2 != 0 {
579                    let expanded;
580                    let payload = if flag & 1 != 0 {
581                        expanded = inflate(&payload, MAX_END_BYTES)?;
582                        expanded.as_slice()
583                    } else {
584                        payload.as_ref()
585                    };
586                    let trailers = end_stream(payload)?;
587                    if !matches!(reader.next().await?, Item::Eof) {
588                        return Err(Status::internal("data follows EndStream"));
589                    }
590                    complete.store(true, Ordering::Relaxed);
591                    Ok(Some((Frame::trailers(trailers), (reader, count, true))))
592                } else {
593                    if count >= MAX_MESSAGES {
594                        return Err(Status::resource_exhausted("response exceeds 256 messages"));
595                    }
596                    Ok(Some((
597                        Frame::data(frame(flag, &payload)),
598                        (reader, count + 1, false),
599                    )))
600                }
601            }
602        },
603    ))
604    .boxed_unsync()
605}
606pub fn failure(status: Status, shape: Shape, close: bool) -> Response<BoxBody> {
607    let value = if shape == Shape::Stream {
608        json!({"error":error_json(&status)})
609    } else {
610        error_json(&status)
611    };
612    let bytes = serde_json::to_vec(&value).expect("bounded error JSON");
613    let mut response = Response::new(body(if shape == Shape::Stream {
614        frame(2, &bytes)
615    } else {
616        bytes.into()
617    }));
618    *response.status_mut() = if shape == Shape::Stream {
619        StatusCode::OK
620    } else {
621        http_status(status.code())
622    };
623    response.headers_mut().insert(
624        "content-type",
625        HeaderValue::from_static(if shape == Shape::Stream {
626            shape.content_type()
627        } else {
628            "application/json"
629        }),
630    );
631    if close {
632        response
633            .headers_mut()
634            .insert("connection", HeaderValue::from_static("close"));
635    }
636    response
637}
638pub async fn server_response(
639    response: Response<BoxBody>,
640    shape: Shape,
641    metadata: RpcMetadata,
642) -> Result<Response<BoxBody>, Status> {
643    let gzip = encoding(response.headers(), "grpc-encoding")?;
644    if let Some(status) = Status::from_header_map(response.headers()) {
645        return Ok(failure(status, shape, false));
646    }
647    let mut reader = Reader::new(response.into_body(), true);
648    if shape == Shape::Unary {
649        let mut result = None;
650        let mut count = 0;
651        let status = loop {
652            match reader.next().await? {
653                Item::Data(flag, payload) => {
654                    count += 1;
655                    if count > 1 {
656                        return Err(Status::internal("unary requires one response"));
657                    }
658                    if flag == 1 && !gzip {
659                        return Err(Status::internal("unnegotiated response compression"));
660                    }
661                    result = Some((flag, payload));
662                }
663                Item::Trailers(headers) => {
664                    break Status::from_header_map(&headers)
665                        .ok_or_else(|| Status::internal("missing native status"))?
666                }
667                Item::Eof => return Err(Status::internal("missing native status")),
668            }
669        };
670        if !matches!(reader.next().await?, Item::Eof) {
671            return Err(Status::internal("data after native status"));
672        }
673        if status.code() != Code::Ok {
674            let mut response = failure(status, shape, false);
675            let mut md = metadata.response.lock().unwrap_or_else(|e| e.into_inner());
676            md.sealed = true;
677            md.leading.append_headers(response.headers_mut(), "")?;
678            md.trailing
679                .append_headers(response.headers_mut(), "trailer-")?;
680            return Ok(response);
681        }
682        let (flag, payload) =
683            result.ok_or_else(|| Status::internal("unary requires one response"))?;
684        let mut response = Response::new(body(payload));
685        response.headers_mut().insert(
686            "content-type",
687            HeaderValue::from_static(shape.content_type()),
688        );
689        if flag == 1 {
690            response
691                .headers_mut()
692                .insert("content-encoding", HeaderValue::from_static("gzip"));
693        }
694        let mut md = metadata.response.lock().unwrap_or_else(|e| e.into_inner());
695        md.sealed = true;
696        md.leading.append_headers(response.headers_mut(), "")?;
697        md.trailing
698            .append_headers(response.headers_mut(), "trailer-")?;
699        Ok(response)
700    } else {
701        // Poll once before sending headers, allowing opening controls to set leading metadata.
702        let first = reader.next().await?;
703        let mut response = Response::new(tonic::body::empty_body());
704        response.headers_mut().insert(
705            "content-type",
706            HeaderValue::from_static(shape.content_type()),
707        );
708        if gzip {
709            response
710                .headers_mut()
711                .insert("connect-content-encoding", HeaderValue::from_static("gzip"));
712        }
713        {
714            let mut md = metadata.response.lock().unwrap_or_else(|e| e.into_inner());
715            md.sealed = true;
716            md.leading.append_headers(response.headers_mut(), "")?;
717        }
718        *response.body_mut() = StreamBody::new(futures::stream::try_unfold(
719            (reader, Some(first), 0usize, false),
720            move |(mut reader, first, count, ended)| {
721                let metadata = metadata.clone();
722                async move {
723                    if ended {
724                        return Ok(None);
725                    }
726                    let item = match first {
727                        Some(item) => item,
728                        None => reader.next().await?,
729                    };
730                    match item {
731                        Item::Data(flag, payload) => {
732                            if count >= MAX_MESSAGES {
733                                return Err(Status::resource_exhausted(
734                                    "response count exceeds 256",
735                                ));
736                            }
737                            Ok(Some((
738                                Frame::data(frame(flag, &payload)),
739                                (reader, None, count + 1, false),
740                            )))
741                        }
742                        Item::Trailers(headers) => {
743                            let status = Status::from_header_map(&headers)
744                                .ok_or_else(|| Status::internal("missing native status"))?;
745                            if !matches!(reader.next().await?, Item::Eof) {
746                                return Err(Status::internal("data after native status"));
747                            }
748                            let md = metadata.response.lock().unwrap_or_else(|e| e.into_inner());
749                            let mut end = json!({"metadata":md.trailing.0});
750                            if status.code() != Code::Ok {
751                                end["error"] = error_json(&status);
752                            }
753                            let bytes = serde_json::to_vec(&end)
754                                .map_err(|_| Status::internal("EndStream encoding failed"))?;
755                            if bytes.len() > MAX_END_BYTES {
756                                return Err(Status::resource_exhausted("EndStream exceeds bound"));
757                            }
758                            Ok(Some((
759                                Frame::data(frame(2, &bytes)),
760                                (reader, None, count, true),
761                            )))
762                        }
763                        Item::Eof => Err(Status::internal("missing native status")),
764                    }
765                }
766            },
767        ))
768        .boxed_unsync();
769        Ok(response)
770    }
771}
772pub async fn client_request(
773    mut request: Request<BoxBody>,
774    shape: Shape,
775) -> Result<Request<BoxBody>, Status> {
776    let gzip = encoding(request.headers(), "grpc-encoding")?;
777    let bytes = collect(
778        std::mem::replace(request.body_mut(), tonic::body::empty_body()),
779        MAX_MESSAGE_BYTES + 5,
780    )
781    .await?;
782    let bytes = streaming_request(&bytes, gzip)?;
783    let timeout = crate::server::grpc::streaming::timeout(
784        request.headers(),
785        std::time::Duration::from_secs(3600),
786    )?;
787    for name in [
788        "grpc-encoding",
789        "grpc-accept-encoding",
790        "grpc-timeout",
791        "content-type",
792        "te",
793        "user-agent",
794    ] {
795        request.headers_mut().remove(name);
796    }
797    request.headers_mut().insert(
798        "content-type",
799        HeaderValue::from_static(shape.content_type()),
800    );
801    request
802        .headers_mut()
803        .insert("connect-protocol-version", HeaderValue::from_static("1"));
804    request.headers_mut().insert(
805        "connect-timeout-ms",
806        HeaderValue::from_str(&timeout.as_millis().max(1).to_string()).unwrap(),
807    );
808    let actual_gzip = bytes[0] == 1;
809    if shape == Shape::Unary {
810        *request.body_mut() = body(bytes.slice(5..));
811        if actual_gzip {
812            request
813                .headers_mut()
814                .insert("content-encoding", HeaderValue::from_static("gzip"));
815        }
816        request
817            .headers_mut()
818            .insert("accept-encoding", HeaderValue::from_static("gzip"));
819    } else {
820        *request.body_mut() = body(bytes);
821        if gzip {
822            request
823                .headers_mut()
824                .insert("connect-content-encoding", HeaderValue::from_static("gzip"));
825        }
826        request
827            .headers_mut()
828            .insert("connect-accept-encoding", HeaderValue::from_static("gzip"));
829    }
830    Ok(request)
831}
832pub async fn client_response(
833    response: Response<BoxBody>,
834    shape: Shape,
835    complete: Arc<AtomicBool>,
836) -> Result<Response<BoxBody>, Status> {
837    if !bounded_headers(response.headers()) {
838        return Err(Status::resource_exhausted("response headers exceed bound"));
839    }
840    let status = response.status();
841    let ct = unique(response.headers(), "content-type")?.map(str::to_owned);
842    let gzip = encoding(
843        response.headers(),
844        if shape == Shape::Unary {
845            "content-encoding"
846        } else {
847            "connect-content-encoding"
848        },
849    )?;
850    if shape == Shape::Stream && unique(response.headers(), "content-encoding")?.is_some() {
851        return Err(Status::internal("stream HTTP content encoding excluded"));
852    }
853    let leading = Metadata::from_headers(response.headers(), "")?;
854    let trailing = Metadata::from_headers(response.headers(), "trailer-")?;
855    let (mut parts, body) = response.into_parts();
856    parts.status = StatusCode::OK;
857    parts.headers = HeaderMap::new();
858    parts
859        .headers
860        .insert("content-type", HeaderValue::from_static("application/grpc"));
861    leading.append_headers(&mut parts.headers, "")?;
862    if shape == Shape::Unary || status != StatusCode::OK {
863        let maximum = if status == StatusCode::OK {
864            MAX_MESSAGE_BYTES
865        } else {
866            MAX_END_BYTES
867        };
868        let bytes = collect(body, maximum).await?;
869        if bytes.len() > maximum {
870            return Err(Status::resource_exhausted("response body exceeds bound"));
871        }
872        let expanded;
873        let bytes = if gzip {
874            expanded = inflate(&bytes, maximum)?;
875            expanded.as_slice()
876        } else {
877            bytes.as_ref()
878        };
879        complete.store(true, Ordering::Relaxed);
880        if status != StatusCode::OK {
881            let fallback = Status::new(inferred_code(status), "Connect HTTP error");
882            let error = if ct.as_deref() == Some("application/json") {
883                serde_json::from_slice::<Error>(bytes)
884                    .ok()
885                    .and_then(|e| e.status().ok())
886                    .unwrap_or(fallback)
887            } else {
888                fallback
889            };
890            error.add_header(&mut parts.headers)?;
891            trailing.append_headers(&mut parts.headers, "")?;
892            return Ok(Response::from_parts(parts, tonic::body::empty_body()));
893        }
894        if ct.as_deref() != Some(shape.content_type()) {
895            return Err(Status::internal("expected binary Connect response"));
896        }
897        let mut headers = HeaderMap::new();
898        trailing.append_headers(&mut headers, "")?;
899        Status::ok("").add_header(&mut headers)?;
900        let body = StreamBody::new(futures::stream::iter([
901            Ok::<_, Status>(Frame::data(frame(0, bytes))),
902            Ok(Frame::trailers(headers)),
903        ]))
904        .boxed_unsync();
905        Ok(Response::from_parts(parts, body))
906    } else {
907        if ct.as_deref() != Some(shape.content_type()) {
908            return Err(Status::internal("expected binary Connect stream"));
909        }
910        if gzip {
911            parts
912                .headers
913                .insert("grpc-encoding", HeaderValue::from_static("gzip"));
914        }
915        Ok(Response::from_parts(
916            parts,
917            client_stream_body(body, gzip, complete),
918        ))
919    }
920}