1use std::collections::HashMap;
103use std::marker::PhantomData;
104use std::pin::Pin;
105use std::time::Duration;
106
107use bytes::Bytes;
108use bytes::BytesMut;
109use http::Request;
110use http::Response;
111use http::Uri;
112use http_body::Body;
113use http_body_util::BodyExt;
114use http_body_util::Full;
115use http_body_util::combinators::BoxBody;
116
117use buffa::view::HasMessageView;
118use buffa::view::MessageView;
119use buffa::view::OwnedView;
120use buffa::view::ViewReborrow;
121pub use futures::Stream;
125pub use futures::stream::iter as stream_iter;
129
130mod sealed {
131 pub trait Sealed {}
132 impl<S> Sealed for S where S: super::Stream + Send + 'static {}
133}
134
135#[diagnostic::on_unimplemented(
160 message = "`{Self}` cannot be used as the request stream of a client-streaming call",
161 label = "expected an async `Stream<Item = {Req}> + Send + 'static`",
162 note = "for a collection that is already in hand, wrap it with `connectrpc::stream_iter(...)`",
163 note = "the stream backs the request body, so it must be `Send + 'static`: yield owned messages (no borrows of local data) or feed the call from a channel-backed stream"
164)]
165pub trait ClientRequestStream<Req>: sealed::Sealed + Stream<Item = Req> + Send + 'static {}
166
167impl<S, Req> ClientRequestStream<Req> for S where S: Stream<Item = Req> + Send + 'static {}
168
169use crate::codec::CodecFormat;
170use crate::codec::content_type;
171use crate::codec::encode_json;
172use crate::codec::header as connect_header;
173use crate::compression::CompressionPolicy;
174use crate::compression::CompressionRegistry;
175use crate::envelope::Envelope;
176use crate::error::ConnectError;
177use crate::error::ErrorCode;
178use crate::error::ErrorDetail;
179use crate::protocol::Protocol;
180use crate::protocol::hdr;
181
182pub type BoxFuture<'a, T> = Pin<Box<dyn Future<Output = T> + Send + 'a>>;
184
185pub type ClientBody = BoxBody<Bytes, ConnectError>;
190
191#[inline]
196pub fn full_body(b: Bytes) -> ClientBody {
197 Full::new(b).map_err(|never| match never {}).boxed()
198}
199
200fn find_connect_error_in_chain(
207 mut err: &(dyn std::error::Error + 'static),
208) -> Option<ConnectError> {
209 loop {
210 if let Some(connect_err) = err.downcast_ref::<ConnectError>() {
211 return Some(connect_err.clone());
212 }
213 err = err.source()?;
214 }
215}
216
217fn unavailable_from_transport_error(
226 context: impl std::fmt::Display,
227 err: impl Into<Box<dyn std::error::Error + Send + Sync>>,
228) -> ConnectError {
229 let err = err.into();
230 let message = format!("{context}: {err}");
231 ConnectError::unavailable(message).with_source(err)
232}
233
234fn map_transport_send_error<E>(err: E, context: &str) -> ConnectError
247where
248 E: std::error::Error + Send + Sync + 'static,
249{
250 find_connect_error_in_chain(&err)
251 .unwrap_or_else(|| unavailable_from_transport_error(context, err))
252}
253
254const RESPONSE_BUFFER_TRAILER_SLACK: usize = 64 * 1024;
260
261fn grpc_web_trailer_frame_end(data: &[u8]) -> Option<usize> {
263 let mut offset = 0;
264
265 while data.len().saturating_sub(offset) >= crate::envelope::HEADER_SIZE {
266 let length = u32::from_be_bytes([
267 data[offset + 1],
268 data[offset + 2],
269 data[offset + 3],
270 data[offset + 4],
271 ]) as usize;
272 let frame_end = offset
273 .checked_add(crate::envelope::HEADER_SIZE)?
274 .checked_add(length)?;
275 if frame_end > data.len() {
276 return None;
277 }
278 if data[offset] & 0x80 != 0 {
279 return Some(frame_end);
280 }
281 offset = frame_end;
282 }
283
284 None
285}
286
287pub trait ClientTransport: Clone + Send + Sync + 'static {
292 type ResponseBody: Body<Data = Bytes> + Send + 'static;
294 type Error: std::error::Error + Send + Sync + 'static;
304
305 fn send(
307 &self,
308 request: Request<ClientBody>,
309 ) -> BoxFuture<'static, Result<Response<Self::ResponseBody>, Self::Error>>;
310}
311
312#[derive(Clone)]
314pub struct ServiceTransport<S> {
315 service: S,
316}
317
318impl<S> ServiceTransport<S> {
319 pub fn new(service: S) -> Self {
321 Self { service }
322 }
323
324 pub fn inner(&self) -> &S {
326 &self.service
327 }
328
329 pub fn inner_mut(&mut self) -> &mut S {
331 &mut self.service
332 }
333
334 pub fn into_inner(self) -> S {
336 self.service
337 }
338}
339
340impl<S, ResBody> ClientTransport for ServiceTransport<S>
341where
342 S: tower::Service<Request<ClientBody>, Response = Response<ResBody>>
343 + Clone
344 + Send
345 + Sync
346 + 'static,
347 S::Error: std::error::Error + Send + Sync + 'static,
348 S::Future: Send + 'static,
349 ResBody: Body<Data = Bytes> + Send + 'static,
350 ResBody::Error: std::error::Error + Send + Sync + 'static,
351{
352 type ResponseBody = ResBody;
353 type Error = S::Error;
354
355 fn send(
356 &self,
357 request: Request<ClientBody>,
358 ) -> BoxFuture<'static, Result<Response<Self::ResponseBody>, Self::Error>> {
359 use tower::ServiceExt;
364 let service = self.service.clone();
365 Box::pin(service.oneshot(request))
366 }
367}
368
369#[cfg(feature = "client")]
371mod http2;
372#[cfg(feature = "client")]
373#[cfg_attr(docsrs, doc(cfg(feature = "client")))]
374pub use http2::Http2Connection;
375#[cfg(feature = "client")]
376#[cfg_attr(docsrs, doc(cfg(feature = "client")))]
377pub use http2::Http2ConnectionBuilder;
378#[cfg(feature = "client")]
379#[cfg_attr(docsrs, doc(cfg(feature = "client")))]
380pub use http2::SharedHttp2Connection;
381#[cfg(feature = "client")]
382#[cfg_attr(docsrs, doc(cfg(feature = "client")))]
383pub use http2::{DEFAULT_ESTABLISHMENT_TIMEOUT, DEFAULT_TCP_CONNECT_TIMEOUT};
384
385#[cfg(feature = "client")]
412#[cfg_attr(docsrs, doc(cfg(feature = "client")))]
413#[derive(Clone)]
414pub struct HttpClient {
415 inner: HttpClientInner,
416}
417
418#[cfg(feature = "client")]
421#[cfg_attr(docsrs, doc(cfg(feature = "client")))]
422impl std::fmt::Debug for HttpClient {
423 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
424 let mode = match self.inner {
425 HttpClientInner::Plain(_) => "plaintext",
426 #[cfg(feature = "client-tls")]
427 HttpClientInner::Tls(_) => "tls",
428 };
429 f.debug_struct("HttpClient").field("mode", &mode).finish()
430 }
431}
432
433#[cfg(feature = "client")]
440#[derive(Clone)]
441enum HttpClientInner {
442 Plain(
444 hyper_util::client::legacy::Client<
445 TimeoutConnector<hyper_util::client::legacy::connect::HttpConnector>,
446 ClientBody,
447 >,
448 ),
449 #[cfg(feature = "client-tls")]
452 Tls(
453 hyper_util::client::legacy::Client<
454 TimeoutConnector<
455 hyper_rustls::HttpsConnector<hyper_util::client::legacy::connect::HttpConnector>,
456 >,
457 ClientBody,
458 >,
459 ),
460}
461
462#[cfg(feature = "client")]
471#[derive(Clone)]
472struct TimeoutConnector<C> {
473 inner: C,
474 timeout: Option<Duration>,
475}
476
477#[cfg(feature = "client")]
478impl<C> tower::Service<Uri> for TimeoutConnector<C>
479where
480 C: tower::Service<Uri>,
481 C::Error: Into<Box<dyn std::error::Error + Send + Sync>> + 'static,
482 C::Future: Send + 'static,
483 C::Response: Send + 'static,
484{
485 type Response = C::Response;
486 type Error = Box<dyn std::error::Error + Send + Sync>;
487 type Future = BoxFuture<'static, Result<C::Response, Self::Error>>;
488
489 fn poll_ready(
490 &mut self,
491 cx: &mut std::task::Context<'_>,
492 ) -> std::task::Poll<Result<(), Self::Error>> {
493 self.inner.poll_ready(cx).map_err(Into::into)
494 }
495
496 fn call(&mut self, uri: Uri) -> Self::Future {
497 let fut = self.inner.call(uri);
498 let timeout = self.timeout;
499 Box::pin(http2::run_establishment(fut, timeout))
500 }
501}
502
503#[cfg(feature = "client")]
504#[cfg_attr(docsrs, doc(cfg(feature = "client")))]
505impl HttpClient {
506 pub fn builder() -> HttpClientBuilder {
513 HttpClientBuilder::default()
514 }
515
516 pub fn plaintext() -> Self {
529 Self::builder().plaintext()
530 }
531
532 pub fn plaintext_http2_only() -> Self {
550 Self::builder().plaintext_http2_only()
551 }
552
553 #[cfg(feature = "client-tls")]
590 #[cfg_attr(docsrs, doc(cfg(feature = "client-tls")))]
591 pub fn with_tls(tls_config: std::sync::Arc<rustls::ClientConfig>) -> Self {
592 Self::builder().with_tls(tls_config)
593 }
594}
595
596#[cfg(feature = "client")]
602#[cfg_attr(docsrs, doc(cfg(feature = "client")))]
603#[derive(Debug, Clone)]
604#[must_use = "call a terminal (plaintext / plaintext_http2_only / with_tls) to build the client"]
605pub struct HttpClientBuilder {
606 tcp_connect_timeout: Option<Duration>,
607 establishment_timeout: Option<Duration>,
608}
609
610#[cfg(feature = "client")]
611impl Default for HttpClientBuilder {
612 fn default() -> Self {
619 Self {
620 tcp_connect_timeout: Some(DEFAULT_TCP_CONNECT_TIMEOUT),
621 establishment_timeout: Some(DEFAULT_ESTABLISHMENT_TIMEOUT),
622 }
623 }
624}
625
626#[cfg(feature = "client")]
627#[cfg_attr(docsrs, doc(cfg(feature = "client")))]
628impl HttpClientBuilder {
629 #[doc(alias = "connect_timeout")]
647 pub fn tcp_connect_timeout(mut self, dur: Duration) -> Self {
648 self.tcp_connect_timeout = http2::finite(dur);
649 self
650 }
651
652 pub fn connect_timeout(self, dur: Duration) -> Self {
654 self.tcp_connect_timeout(dur)
655 }
656
657 pub fn no_tcp_connect_timeout(mut self) -> Self {
661 self.tcp_connect_timeout = None;
662 self
663 }
664
665 pub fn establishment_timeout(mut self, dur: Duration) -> Self {
691 self.establishment_timeout = http2::finite(dur);
692 self
693 }
694
695 pub fn no_establishment_timeout(mut self) -> Self {
701 self.establishment_timeout = None;
702 self
703 }
704
705 fn http_connector(&self) -> hyper_util::client::legacy::connect::HttpConnector {
706 let mut connector = hyper_util::client::legacy::connect::HttpConnector::new();
707 connector.set_nodelay(true);
708 connector.set_connect_timeout(self.tcp_connect_timeout);
709 connector
710 }
711
712 fn wrap<C>(&self, connector: C) -> TimeoutConnector<C> {
714 TimeoutConnector {
715 inner: connector,
716 timeout: self.establishment_timeout,
717 }
718 }
719
720 #[must_use]
722 pub fn plaintext(self) -> HttpClient {
723 use hyper_util::client::legacy::Client;
724 use hyper_util::rt::TokioExecutor;
725
726 let connector = self.wrap(self.http_connector());
727 let client = Client::builder(TokioExecutor::new()).build(connector);
728 HttpClient {
729 inner: HttpClientInner::Plain(client),
730 }
731 }
732
733 #[must_use]
736 pub fn plaintext_http2_only(self) -> HttpClient {
737 use hyper_util::client::legacy::Client;
738 use hyper_util::rt::TokioExecutor;
739
740 let connector = self.wrap(self.http_connector());
741 let client = Client::builder(TokioExecutor::new())
742 .http2_only(true)
743 .build(connector);
744 HttpClient {
745 inner: HttpClientInner::Plain(client),
746 }
747 }
748
749 #[cfg(feature = "client-tls")]
751 #[cfg_attr(docsrs, doc(cfg(feature = "client-tls")))]
752 #[must_use]
753 pub fn with_tls(self, tls_config: std::sync::Arc<rustls::ClientConfig>) -> HttpClient {
754 use hyper_util::client::legacy::Client;
755 use hyper_util::rt::TokioExecutor;
756
757 let mut http = self.http_connector();
758 http.enforce_http(false);
761
762 let mut cfg = (*tls_config).clone();
768 cfg.alpn_protocols.clear();
769
770 let https = hyper_rustls::HttpsConnectorBuilder::new()
774 .with_tls_config(cfg)
775 .https_only()
776 .enable_all_versions()
777 .wrap_connector(http);
778
779 let connector = self.wrap(https);
780 let client = Client::builder(TokioExecutor::new()).build(connector);
781 HttpClient {
782 inner: HttpClientInner::Tls(client),
783 }
784 }
785}
786
787#[cfg(feature = "client")]
792#[cfg_attr(docsrs, doc(cfg(feature = "client")))]
793impl ClientTransport for HttpClient {
794 type ResponseBody = hyper::body::Incoming;
795 type Error = ConnectError;
796
797 fn send(
798 &self,
799 request: Request<ClientBody>,
800 ) -> BoxFuture<'static, Result<Response<Self::ResponseBody>, Self::Error>> {
801 let scheme = request.uri().scheme_str();
802
803 match &self.inner {
804 HttpClientInner::Plain(client) => {
805 if scheme == Some("https") {
808 return Box::pin(async {
809 Err(ConnectError::invalid_argument(
810 "HttpClient::plaintext() received https:// URI; \
811 use HttpClient::with_tls for TLS",
812 ))
813 });
814 }
815 let client = client.clone();
816 Box::pin(async move {
817 client
818 .request(request)
819 .await
820 .map_err(|e| unavailable_from_transport_error("HTTP request failed", e))
821 })
822 }
823 #[cfg(feature = "client-tls")]
824 HttpClientInner::Tls(client) => {
825 if scheme == Some("http") {
828 return Box::pin(async {
829 Err(ConnectError::invalid_argument(
830 "HttpClient::with_tls() received http:// URI; \
831 use HttpClient::plaintext for cleartext",
832 ))
833 });
834 }
835 let client = client.clone();
836 Box::pin(async move {
837 client
838 .request(request)
839 .await
840 .map_err(|e| unavailable_from_transport_error("HTTPS request failed", e))
841 })
842 }
843 }
844 }
845}
846
847#[derive(Clone, Debug)]
865#[non_exhaustive]
866pub struct ClientConfig {
867 pub(crate) base_uri: Uri,
868 pub(crate) protocol: Protocol,
869 pub(crate) codec_format: CodecFormat,
870 pub(crate) compression: CompressionRegistry,
871 pub(crate) request_compression: Option<String>,
872 pub(crate) compression_policy: CompressionPolicy,
873 pub(crate) default_timeout: Option<Duration>,
874 pub(crate) default_max_message_size: Option<usize>,
875 pub(crate) default_headers: http::HeaderMap,
876}
877
878impl ClientConfig {
879 pub fn new(base_uri: Uri) -> Self {
883 Self {
884 base_uri,
885 protocol: Protocol::Connect,
886 codec_format: CodecFormat::Proto,
887 compression: CompressionRegistry::default(),
888 request_compression: None,
889 compression_policy: CompressionPolicy::default(),
890 default_timeout: None,
891 default_max_message_size: None,
892 default_headers: http::HeaderMap::new(),
893 }
894 }
895
896 #[must_use]
902 pub fn with_protocol(mut self, protocol: Protocol) -> Self {
903 self.protocol = protocol;
904 self
905 }
906
907 #[must_use]
918 pub fn with_codec_format(mut self, format: CodecFormat) -> Self {
919 self.codec_format = format;
920 self
921 }
922
923 #[cfg(feature = "json")]
928 #[cfg_attr(docsrs, doc(cfg(feature = "json")))]
929 #[must_use]
930 pub fn json(mut self) -> Self {
931 self.codec_format = CodecFormat::Json;
932 self
933 }
934
935 #[must_use]
937 pub fn proto(mut self) -> Self {
938 self.codec_format = CodecFormat::Proto;
939 self
940 }
941
942 #[must_use]
946 pub fn with_compression(mut self, registry: CompressionRegistry) -> Self {
947 self.compression = registry;
948 self
949 }
950
951 #[must_use]
955 pub fn compress_requests(mut self, encoding: impl Into<String>) -> Self {
956 self.request_compression = Some(encoding.into());
957 self
958 }
959
960 #[must_use]
964 pub fn with_compression_policy(mut self, policy: CompressionPolicy) -> Self {
965 self.compression_policy = policy;
966 self
967 }
968
969 #[must_use]
974 pub fn with_default_timeout(mut self, timeout: Duration) -> Self {
975 self.default_timeout = Some(timeout);
976 self
977 }
978
979 #[must_use]
984 pub fn with_default_max_message_size(mut self, size: usize) -> Self {
985 self.default_max_message_size = Some(size);
986 self
987 }
988
989 #[must_use]
998 pub fn with_default_header(
999 mut self,
1000 name: impl TryInto<http::header::HeaderName>,
1001 value: impl TryInto<http::header::HeaderValue>,
1002 ) -> Self {
1003 if let (Ok(name), Ok(value)) = (name.try_into(), value.try_into()) {
1004 self.default_headers.append(name, value);
1005 }
1006 self
1007 }
1008
1009 #[must_use]
1013 pub fn with_default_headers(mut self, headers: http::HeaderMap) -> Self {
1014 self.default_headers = headers;
1015 self
1016 }
1017
1018 pub fn base_uri(&self) -> &Uri {
1024 &self.base_uri
1025 }
1026
1027 pub fn protocol(&self) -> Protocol {
1031 self.protocol
1032 }
1033
1034 pub fn codec_format(&self) -> CodecFormat {
1038 self.codec_format
1039 }
1040
1041 pub fn compression(&self) -> &CompressionRegistry {
1045 &self.compression
1046 }
1047
1048 pub fn request_compression(&self) -> Option<&str> {
1052 self.request_compression.as_deref()
1053 }
1054
1055 pub fn compression_policy(&self) -> CompressionPolicy {
1059 self.compression_policy
1060 }
1061
1062 pub fn default_timeout(&self) -> Option<Duration> {
1067 self.default_timeout
1068 }
1069
1070 pub fn default_max_message_size(&self) -> Option<usize> {
1075 self.default_max_message_size
1076 }
1077
1078 pub fn default_headers(&self) -> &http::HeaderMap {
1086 &self.default_headers
1087 }
1088}
1089
1090#[derive(Debug, Clone, Default)]
1112#[non_exhaustive]
1113pub struct CallOptions {
1114 pub(crate) headers: http::HeaderMap,
1115 pub(crate) timeout: Option<Duration>,
1116 pub(crate) max_message_size: Option<usize>,
1117 pub(crate) compress: Option<bool>,
1118}
1119
1120impl CallOptions {
1121 #[must_use]
1127 pub fn with_timeout(mut self, timeout: Duration) -> Self {
1128 self.timeout = Some(timeout);
1129 self
1130 }
1131
1132 #[must_use]
1140 pub fn with_header(
1141 mut self,
1142 name: impl TryInto<http::header::HeaderName>,
1143 value: impl TryInto<http::header::HeaderValue>,
1144 ) -> Self {
1145 if let (Ok(name), Ok(value)) = (name.try_into(), value.try_into()) {
1146 self.headers.append(name, value);
1147 }
1148 self
1149 }
1150
1151 pub fn try_with_header(
1160 mut self,
1161 name: impl TryInto<http::header::HeaderName>,
1162 value: impl TryInto<http::header::HeaderValue>,
1163 ) -> Result<Self, ConnectError> {
1164 let name = name
1165 .try_into()
1166 .map_err(|_| ConnectError::internal("invalid header name"))?;
1167 let value = value
1168 .try_into()
1169 .map_err(|_| ConnectError::internal("invalid header value"))?;
1170 self.headers.append(name, value);
1171 Ok(self)
1172 }
1173
1174 #[must_use]
1178 pub fn with_headers(
1179 mut self,
1180 headers: impl IntoIterator<Item = (http::header::HeaderName, http::header::HeaderValue)>,
1181 ) -> Self {
1182 for (name, value) in headers {
1183 self.headers.append(name, value);
1184 }
1185 self
1186 }
1187
1188 #[must_use]
1192 pub fn with_max_message_size(mut self, size: usize) -> Self {
1193 self.max_message_size = Some(size);
1194 self
1195 }
1196
1197 #[must_use]
1203 pub fn with_compress(mut self, enabled: bool) -> Self {
1204 self.compress = Some(enabled);
1205 self
1206 }
1207
1208 pub fn headers(&self) -> &http::HeaderMap {
1217 &self.headers
1218 }
1219
1220 pub fn timeout(&self) -> Option<Duration> {
1224 self.timeout
1225 }
1226
1227 pub fn max_message_size(&self) -> Option<usize> {
1234 self.max_message_size
1235 }
1236
1237 pub fn compress(&self) -> Option<bool> {
1242 self.compress
1243 }
1244}
1245
1246fn effective_options(config: &ClientConfig, options: CallOptions) -> CallOptions {
1256 CallOptions {
1257 timeout: options.timeout.or(config.default_timeout),
1258 max_message_size: options.max_message_size.or(config.default_max_message_size),
1259 compress: options.compress,
1260 headers: merge_headers(&config.default_headers, options.headers),
1261 }
1262}
1263
1264fn merge_headers(config_defaults: &http::HeaderMap, options: http::HeaderMap) -> http::HeaderMap {
1271 if config_defaults.is_empty() {
1273 return options;
1274 }
1275 if options.is_empty() {
1277 return config_defaults.clone();
1278 }
1279
1280 let mut merged = config_defaults.clone();
1281 for name in options.keys() {
1284 merged.remove(name);
1285 }
1286 for (name, value) in options.iter() {
1287 merged.append(name.clone(), value.clone());
1288 }
1289 merged
1290}
1291
1292const CONNECT_TIMEOUT_MAX_MILLIS: u64 = 9_999_999_999;
1293const GRPC_TIMEOUT_MAX_SECONDS: u64 = 99_999_999;
1294
1295#[derive(Debug, Clone, Copy, PartialEq, Eq)]
1296enum EncodedTimeout {
1297 Connect {
1298 millis: u64,
1299 },
1300 Grpc {
1301 value: u64,
1302 unit: char,
1303 duration: Duration,
1304 },
1305}
1306
1307impl EncodedTimeout {
1308 fn duration(self) -> Duration {
1309 match self {
1310 Self::Connect { millis } => Duration::from_millis(millis),
1311 Self::Grpc { duration, .. } => duration,
1312 }
1313 }
1314
1315 fn header_value(self) -> String {
1316 match self {
1317 Self::Connect { millis } => millis.to_string(),
1318 Self::Grpc { value, unit, .. } => format!("{value}{unit}"),
1319 }
1320 }
1321}
1322
1323fn grpc_encoded_timeout(value: u128, unit: char, duration: Duration) -> EncodedTimeout {
1324 EncodedTimeout::Grpc {
1325 value: value as u64,
1326 unit,
1327 duration,
1328 }
1329}
1330
1331#[allow(clippy::manual_is_multiple_of)]
1334fn encoded_timeout(timeout: Duration, protocol: Protocol) -> EncodedTimeout {
1335 match protocol {
1336 Protocol::Connect => EncodedTimeout::Connect {
1337 millis: timeout.as_millis().min(CONNECT_TIMEOUT_MAX_MILLIS as u128) as u64,
1338 },
1339 Protocol::Grpc | Protocol::GrpcWeb => {
1340 let max = GRPC_TIMEOUT_MAX_SECONDS as u128;
1341 let nanos = timeout.as_nanos();
1342 let secs = timeout.as_secs() as u128;
1343 let millis = timeout.as_millis();
1344 let micros = timeout.as_micros();
1345
1346 if nanos == 0 {
1347 grpc_encoded_timeout(0, 'n', Duration::ZERO)
1348 } else if nanos % 1_000_000_000 == 0 && secs <= max {
1349 grpc_encoded_timeout(secs, 'S', Duration::from_secs(secs as u64))
1350 } else if nanos % 1_000_000 == 0 && millis <= max {
1351 grpc_encoded_timeout(millis, 'm', Duration::from_millis(millis as u64))
1352 } else if nanos % 1_000 == 0 && micros <= max {
1353 grpc_encoded_timeout(micros, 'u', Duration::from_micros(micros as u64))
1354 } else if nanos <= max {
1355 grpc_encoded_timeout(nanos, 'n', Duration::from_nanos(nanos as u64))
1356 } else if micros <= max {
1357 grpc_encoded_timeout(micros, 'u', Duration::from_micros(micros as u64))
1358 } else if millis <= max {
1359 grpc_encoded_timeout(millis, 'm', Duration::from_millis(millis as u64))
1360 } else if secs <= max {
1361 grpc_encoded_timeout(secs, 'S', Duration::from_secs(secs as u64))
1362 } else {
1363 grpc_encoded_timeout(max, 'S', Duration::from_secs(GRPC_TIMEOUT_MAX_SECONDS))
1364 }
1365 }
1366 }
1367}
1368
1369fn client_deadline(timeout: Option<Duration>, protocol: Protocol) -> Option<std::time::Instant> {
1370 timeout
1371 .map(|t| encoded_timeout(t, protocol).duration())
1372 .and_then(|t| std::time::Instant::now().checked_add(t))
1373}
1374
1375async fn with_deadline<F, T>(
1387 deadline: Option<std::time::Instant>,
1388 fut: F,
1389) -> Result<T, ConnectError>
1390where
1391 F: Future<Output = Result<T, ConnectError>>,
1392{
1393 match deadline {
1394 None => fut.await,
1395 Some(d) => {
1396 let tokio_deadline = tokio::time::Instant::from_std(d);
1398 tokio::time::timeout_at(tokio_deadline, fut)
1399 .await
1400 .map_err(|_| ConnectError::deadline_exceeded("client-side deadline exceeded"))?
1401 }
1402 }
1403}
1404
1405fn deadline_elapsed(deadline: Option<std::time::Instant>) -> bool {
1417 deadline.is_some_and(|d| std::time::Instant::now() >= d)
1418}
1419
1420fn classify_body_read_error(
1436 context: &str,
1437 error: &dyn std::fmt::Display,
1438 deadline: Option<std::time::Instant>,
1439) -> ConnectError {
1440 if deadline_elapsed(deadline) {
1441 ConnectError::deadline_exceeded(format!("{context} after the deadline elapsed: {error}"))
1442 } else {
1443 ConnectError::internal(format!("{context}: {error}"))
1444 }
1445}
1446
1447#[derive(Debug)]
1452pub struct UnaryResponse<Resp> {
1453 headers: http::HeaderMap,
1454 body: Resp,
1455 trailers: http::HeaderMap,
1456}
1457
1458impl<Resp> UnaryResponse<Resp> {
1459 #[must_use]
1461 pub fn headers(&self) -> &http::HeaderMap {
1462 &self.headers
1463 }
1464
1465 #[must_use]
1474 pub fn into_view(self) -> Resp {
1475 self.body
1476 }
1477
1478 #[must_use]
1480 pub fn trailers(&self) -> &http::HeaderMap {
1481 &self.trailers
1482 }
1483
1484 #[must_use]
1486 pub fn into_parts(self) -> (http::HeaderMap, Resp, http::HeaderMap) {
1487 (self.headers, self.body, self.trailers)
1488 }
1489}
1490
1491impl<V> UnaryResponse<OwnedView<V>>
1494where
1495 V: MessageView<'static>,
1496{
1497 #[must_use]
1512 pub fn into_owned(self) -> V::Owned {
1513 self.into_owned_parts().1
1514 }
1515
1516 #[must_use]
1526 pub fn into_owned_parts(self) -> (http::HeaderMap, V::Owned, http::HeaderMap) {
1527 (self.headers, self.body.to_owned_message(), self.trailers)
1528 }
1529}
1530
1531impl<V> UnaryResponse<OwnedView<V>>
1534where
1535 V: ViewReborrow,
1536{
1537 #[must_use]
1550 pub fn view(&self) -> &V::Reborrowed<'_> {
1551 self.body.reborrow()
1552 }
1553}
1554
1555fn decode_response_view<RespView>(
1563 data: Bytes,
1564 format: CodecFormat,
1565) -> Result<OwnedView<RespView>, ConnectError>
1566where
1567 RespView: MessageView<'static> + Send,
1568 RespView::Owned: buffa::Message + crate::codec::JsonDeserialize,
1569{
1570 match format {
1571 CodecFormat::Proto => OwnedView::<RespView>::decode(data)
1572 .map_err(|e| ConnectError::internal(format!("failed to decode response: {e}"))),
1573 #[cfg(feature = "json")]
1574 CodecFormat::Json => {
1575 let owned: RespView::Owned = serde_json::from_slice(&data).map_err(|e| {
1576 ConnectError::internal(format!("failed to decode JSON response: {e}"))
1577 })?;
1578 OwnedView::<RespView>::from_owned(&owned)
1579 .map_err(|e| ConnectError::internal(format!("failed to re-encode for view: {e}")))
1580 }
1581 #[cfg(not(feature = "json"))]
1582 CodecFormat::Json => Err(ConnectError::unimplemented(
1583 crate::codec::JSON_FEATURE_DISABLED,
1584 )),
1585 }
1586}
1587
1588pub async fn call_unary<T, Req, RespView>(
1593 transport: &T,
1594 config: &ClientConfig,
1595 service: &str,
1596 method: &str,
1597 request: Req,
1598 options: CallOptions,
1599) -> Result<UnaryResponse<OwnedView<RespView>>, ConnectError>
1600where
1601 T: ClientTransport,
1602 <T::ResponseBody as Body>::Error: std::fmt::Display,
1603 Req: buffa::Message + crate::codec::JsonSerialize,
1604 RespView: MessageView<'static> + Send,
1605 RespView::Owned: buffa::Message + crate::codec::JsonDeserialize,
1606{
1607 let options = effective_options(config, options);
1608
1609 let base_str = config.base_uri.to_string();
1611 let base_str = base_str.trim_end_matches('/');
1612 let full_uri = format!("{base_str}/{service}/{method}");
1613 let uri: Uri = full_uri
1614 .parse()
1615 .map_err(|e| ConnectError::internal(format!("invalid URI: {e}")))?;
1616
1617 let body = match config.codec_format {
1619 CodecFormat::Proto => request.encode_to_bytes(),
1620 CodecFormat::Json => encode_json(&request)?,
1621 };
1622
1623 let (body, applied_content_encoding) = match config.protocol {
1633 Protocol::Grpc | Protocol::GrpcWeb => {
1634 let compression_for_encoder = config.request_compression.as_ref().map(|enc| {
1635 (
1636 std::sync::Arc::new(config.compression.clone()),
1637 enc.as_str(),
1638 )
1639 });
1640 let mut encoder = crate::envelope::EnvelopeEncoder::new(
1641 compression_for_encoder,
1642 config.compression_policy.with_override(options.compress),
1643 );
1644 let mut buf = bytes::BytesMut::new();
1645 tokio_util::codec::Encoder::encode(&mut encoder, body, &mut buf)
1646 .map_err(|e| ConnectError::internal(format!("envelope encode failed: {e}")))?;
1647 (buf.freeze(), None)
1648 }
1649 Protocol::Connect => {
1650 if let Some(ref encoding) = config.request_compression {
1651 let effective_policy = config.compression_policy.with_override(options.compress);
1652 if effective_policy.should_compress(body.len()) {
1653 let compressed = config.compression.compress(encoding, &body)?;
1654 (compressed, Some(encoding.as_str()))
1655 } else {
1656 (body, None)
1657 }
1658 } else {
1659 (body, None)
1660 }
1661 }
1662 };
1663
1664 let deadline = client_deadline(options.timeout, config.protocol);
1669
1670 let mut builder = Request::builder().method(http::Method::POST).uri(uri);
1672 builder = add_unary_request_headers(builder, config, options.timeout, applied_content_encoding);
1673
1674 let headers = builder.headers_mut().unwrap();
1676 for (name, value) in &options.headers {
1677 headers.append(name.clone(), value.clone());
1678 }
1679
1680 let http_request = builder
1681 .body(full_body(body))
1682 .map_err(|e| ConnectError::internal(format!("failed to build request: {e}")))?;
1683
1684 with_deadline(deadline, async {
1688 let response = transport
1689 .send(http_request)
1690 .await
1691 .map_err(|e| map_transport_send_error(e, "request failed"))?;
1692
1693 match config.protocol {
1694 Protocol::Connect => {
1695 parse_connect_unary_response(response, config, &options, deadline).await
1696 }
1697 Protocol::Grpc | Protocol::GrpcWeb => {
1698 parse_grpc_unary_response(response, config, &options, deadline).await
1699 }
1700 }
1701 })
1702 .await
1703}
1704
1705pub async fn call_unary_get<T, Req, RespView>(
1730 transport: &T,
1731 config: &ClientConfig,
1732 service: &str,
1733 method: &str,
1734 request: Req,
1735 options: CallOptions,
1736) -> Result<UnaryResponse<OwnedView<RespView>>, ConnectError>
1737where
1738 T: ClientTransport,
1739 <T::ResponseBody as Body>::Error: std::fmt::Display,
1740 Req: buffa::Message + crate::codec::JsonSerialize,
1741 RespView: MessageView<'static> + Send,
1742 RespView::Owned: buffa::Message + crate::codec::JsonDeserialize,
1743{
1744 if !matches!(config.protocol, Protocol::Connect) {
1746 return Err(ConnectError::invalid_argument(
1747 "call_unary_get requires Protocol::Connect (gRPC/gRPC-Web are POST-only)",
1748 ));
1749 }
1750
1751 let options = effective_options(config, options);
1752
1753 let base_str = config.base_uri.to_string();
1755 let base_str = base_str.trim_end_matches('/');
1756
1757 let body = match config.codec_format {
1759 CodecFormat::Proto => request.encode_to_bytes(),
1760 CodecFormat::Json => encode_json(&request)?,
1761 };
1762
1763 let (payload, compressed_with) = if let Some(ref encoding) = config.request_compression {
1765 let effective_policy = config.compression_policy.with_override(options.compress);
1766 if effective_policy.should_compress(body.len()) {
1767 let compressed = config.compression.compress(encoding, &body)?;
1768 (compressed, Some(encoding.as_str()))
1769 } else {
1770 (body, None)
1771 }
1772 } else {
1773 (body, None)
1774 };
1775
1776 let is_binary_codec = matches!(config.codec_format, CodecFormat::Proto);
1780 let use_base64 = is_binary_codec || compressed_with.is_some();
1781
1782 let encoded_message = if use_base64 {
1783 use base64::Engine;
1786 base64::engine::general_purpose::URL_SAFE_NO_PAD.encode(&payload)
1787 } else {
1788 percent_encoding::percent_encode(&payload, percent_encoding::NON_ALPHANUMERIC).to_string()
1792 };
1793
1794 let encoding_name = match config.codec_format {
1795 CodecFormat::Proto => "proto",
1796 CodecFormat::Json => "json",
1797 };
1798
1799 let query =
1800 build_connect_get_query(use_base64, compressed_with, encoding_name, &encoded_message);
1801
1802 let full_uri = format!("{base_str}/{service}/{method}?{query}");
1803 let uri: Uri = full_uri
1804 .parse()
1805 .map_err(|e| ConnectError::internal(format!("invalid GET URI: {e}")))?;
1806
1807 let deadline = client_deadline(options.timeout, Protocol::Connect);
1808
1809 let mut builder = Request::builder().method(http::Method::GET).uri(uri);
1813 if let Some(timeout) = options.timeout {
1814 builder = builder.header(
1815 crate::codec::header::TIMEOUT_MS,
1816 format_timeout(timeout, Protocol::Connect),
1817 );
1818 }
1819 let accept = config.compression.accept_encoding_header();
1821 if !accept.is_empty() {
1822 builder = builder.header(http::header::ACCEPT_ENCODING, accept);
1823 }
1824
1825 let headers = builder.headers_mut().unwrap();
1827 for (name, value) in &options.headers {
1828 headers.append(name.clone(), value.clone());
1829 }
1830
1831 let http_request = builder
1832 .body(full_body(Bytes::new()))
1833 .map_err(|e| ConnectError::internal(format!("failed to build GET request: {e}")))?;
1834
1835 with_deadline(deadline, async {
1836 let response = transport
1837 .send(http_request)
1838 .await
1839 .map_err(|e| map_transport_send_error(e, "GET request failed"))?;
1840
1841 parse_connect_unary_response(response, config, &options, deadline).await
1843 })
1844 .await
1845}
1846
1847fn build_connect_get_query(
1858 use_base64: bool,
1859 compression: Option<&str>,
1860 encoding: &str,
1861 encoded_message: &str,
1862) -> String {
1863 let mut query = String::with_capacity(
1864 "connect=v1&encoding=&message=".len()
1865 + if use_base64 { "&base64=1".len() } else { 0 }
1866 + compression.map_or(0, |c| "&compression=".len() + c.len())
1867 + encoding.len()
1868 + encoded_message.len(),
1869 );
1870 query.push_str("connect=v1");
1871 if use_base64 {
1872 query.push_str("&base64=1");
1873 }
1874 if let Some(enc) = compression {
1875 query.push_str("&compression=");
1876 query.push_str(enc);
1877 }
1878 query.push_str("&encoding=");
1879 query.push_str(encoding);
1880 query.push_str("&message=");
1881 query.push_str(encoded_message);
1882 query
1883}
1884
1885fn with_response_metadata<'a>(
1892 headers: &'a http::HeaderMap,
1893 trailers: &'a http::HeaderMap,
1894) -> impl Fn(ConnectError) -> ConnectError + 'a {
1895 move |mut err| {
1896 err.set_response_headers(headers.clone());
1897 if !trailers.is_empty() {
1898 err.set_trailers(trailers.clone());
1899 }
1900 err
1901 }
1902}
1903
1904fn map_response_decompression_error(mut e: ConnectError) -> ConnectError {
1919 match e.code {
1920 ErrorCode::Unimplemented => e.code = ErrorCode::Internal,
1921 ErrorCode::InvalidArgument => e.code = ErrorCode::DataLoss,
1922 _ => {}
1923 }
1924 e
1925}
1926
1927async fn parse_connect_unary_response<B, RespView>(
1929 response: Response<B>,
1930 config: &ClientConfig,
1931 options: &CallOptions,
1932 deadline: Option<std::time::Instant>,
1933) -> Result<UnaryResponse<OwnedView<RespView>>, ConnectError>
1934where
1935 B: Body<Data = Bytes> + Send,
1936 B::Error: std::fmt::Display,
1937 RespView: MessageView<'static> + Send,
1938 RespView::Owned: buffa::Message + crate::codec::JsonDeserialize,
1939{
1940 let status = response.status();
1941 if !status.is_success() {
1942 let response_headers = response.headers().clone();
1943 let mut trailers = http::HeaderMap::new();
1944 let mut headers = http::HeaderMap::new();
1945 for (name, value) in &response_headers {
1946 if let Some(trailer_name) = name.as_str().strip_prefix("trailer-") {
1947 if let Ok(name) = http::header::HeaderName::from_bytes(trailer_name.as_bytes()) {
1948 trailers.append(name, value.clone());
1949 }
1950 } else {
1951 headers.append(name.clone(), value.clone());
1952 }
1953 }
1954
1955 let error_encoding = response_headers
1956 .get(http::header::CONTENT_ENCODING)
1957 .and_then(|v| v.to_str().ok())
1958 .map(|s| s.to_owned());
1959
1960 let max_err_body_size = options
1961 .max_message_size
1962 .unwrap_or(crate::service::DEFAULT_MAX_MESSAGE_SIZE);
1963
1964 let body = collect_body_bounded(response.into_body(), max_err_body_size, deadline)
1965 .await
1966 .map_err(|mut e| {
1967 e.set_response_headers(headers.clone());
1968 e.set_trailers(trailers.clone());
1969 e
1970 })?;
1971
1972 let body = match error_encoding {
1976 Some(encoding) => {
1977 match config
1978 .compression
1979 .decompress_with_limit(&encoding, body, max_err_body_size)
1980 {
1981 Ok(decompressed) => decompressed,
1982 Err(e) => {
1983 tracing::debug!(
1984 "failed to decompress Connect error response ({encoding}): {e}"
1985 );
1986 let mut err = ConnectError::new(
1987 http_status_to_error_code(status),
1988 format!("HTTP error {}", status.as_u16()),
1989 );
1990 err.set_response_headers(headers);
1991 err.set_trailers(trailers);
1992 return Err(err);
1993 }
1994 }
1995 }
1996 None => body,
1997 };
1998
1999 if let Ok(error) = serde_json::from_slice::<ConnectErrorResponse>(&body) {
2000 let code = error
2001 .code
2002 .as_deref()
2003 .and_then(|s| s.parse::<ErrorCode>().ok())
2004 .unwrap_or_else(|| http_status_to_error_code(status));
2005 let mut err = ConnectError::new(code, error.message.unwrap_or_default());
2006 err.details = error.details;
2007 err.set_response_headers(headers);
2008 err.set_trailers(trailers);
2009 return Err(err);
2010 }
2011
2012 let code = http_status_to_error_code(status);
2013 let mut err = ConnectError::new(
2014 code,
2015 format!(
2016 "HTTP error {}: {}",
2017 status.as_u16(),
2018 String::from_utf8_lossy(&body)
2019 ),
2020 );
2021 err.set_response_headers(headers);
2022 err.set_trailers(trailers);
2023 return Err(err);
2024 }
2025
2026 let mut resp_headers = http::HeaderMap::new();
2027 let mut resp_trailers = http::HeaderMap::new();
2028 for (name, value) in response.headers() {
2029 if let Some(trailer_name) = name.as_str().strip_prefix("trailer-") {
2030 if let Ok(name) = http::header::HeaderName::from_bytes(trailer_name.as_bytes()) {
2031 resp_trailers.append(name, value.clone());
2032 }
2033 } else {
2034 resp_headers.append(name.clone(), value.clone());
2035 }
2036 }
2037
2038 let expected_content_type = config.codec_format.content_type();
2039 if let Some(resp_content_type) = response.headers().get(http::header::CONTENT_TYPE) {
2040 let ct = resp_content_type.to_str().unwrap_or("");
2041 if !ct.starts_with(expected_content_type) {
2042 let code = if ct.starts_with(content_type::PROTO) || ct.starts_with(content_type::JSON)
2043 {
2044 ErrorCode::Internal
2045 } else {
2046 ErrorCode::Unknown
2047 };
2048 let mut err = ConnectError::new(code, format!("unexpected content-type: {ct}"));
2049 err.set_response_headers(resp_headers);
2050 err.set_trailers(resp_trailers);
2051 return Err(err);
2052 }
2053 }
2054
2055 let response_encoding = response
2056 .headers()
2057 .get(http::header::CONTENT_ENCODING)
2058 .and_then(|v| v.to_str().ok())
2059 .map(|s| s.to_owned());
2060
2061 let max_message_size = options
2062 .max_message_size
2063 .unwrap_or(crate::service::DEFAULT_MAX_MESSAGE_SIZE);
2064
2065 let attach = with_response_metadata(&resp_headers, &resp_trailers);
2068
2069 let body = collect_body_bounded(response.into_body(), max_message_size, deadline)
2070 .await
2071 .map_err(&attach)?;
2072
2073 let body = if let Some(encoding) = response_encoding {
2074 config
2075 .compression
2076 .decompress_with_limit(&encoding, body, max_message_size)
2077 .map_err(map_response_decompression_error)
2078 .map_err(&attach)?
2079 } else {
2080 body
2081 };
2082
2083 if body.len() > max_message_size {
2084 return Err(attach(ConnectError::new(
2085 ErrorCode::ResourceExhausted,
2086 format!(
2087 "message size {} exceeds limit {}",
2088 body.len(),
2089 max_message_size
2090 ),
2091 )));
2092 }
2093
2094 let message = decode_response_view::<RespView>(body, config.codec_format).map_err(&attach)?;
2095 drop(attach);
2096
2097 Ok(UnaryResponse {
2098 headers: resp_headers,
2099 body: message,
2100 trailers: resp_trailers,
2101 })
2102}
2103
2104async fn parse_grpc_unary_response<B, RespView>(
2109 response: Response<B>,
2110 config: &ClientConfig,
2111 options: &CallOptions,
2112 deadline: Option<std::time::Instant>,
2113) -> Result<UnaryResponse<OwnedView<RespView>>, ConnectError>
2114where
2115 B: Body<Data = Bytes> + Send,
2116 B::Error: std::fmt::Display,
2117 RespView: MessageView<'static> + Send,
2118 RespView::Owned: buffa::Message + crate::codec::JsonDeserialize,
2119{
2120 let status = response.status();
2121 let resp_headers = response.headers().clone();
2122
2123 if !status.is_success() {
2125 let code = http_status_to_error_code(status);
2126 let mut err = ConnectError::new(code, format!("HTTP error {}", status.as_u16()));
2127 err.set_response_headers(resp_headers);
2128 return Err(err);
2129 }
2130
2131 validate_grpc_response_content_type(&resp_headers, config)?;
2132
2133 let response_encoding = resp_headers
2135 .get("grpc-encoding")
2136 .and_then(|v| v.to_str().ok())
2137 .map(|s| s.to_owned());
2138
2139 if let Some(ref enc) = response_encoding
2140 && enc != "identity"
2141 && !config.compression.supports(enc)
2142 {
2143 let mut err = ConnectError::internal(format!("unsupported response compression: {enc}"));
2144 err.set_response_headers(resp_headers);
2145 return Err(err);
2146 }
2147
2148 let mut body = std::pin::pin!(response.into_body());
2151 let mut buf = BytesMut::new();
2152 let mut grpc_trailers = http::HeaderMap::new();
2153 let mut has_body_data = false;
2154 let max_buf_size = options
2157 .max_message_size
2158 .unwrap_or(crate::service::DEFAULT_MAX_MESSAGE_SIZE)
2159 .saturating_add(crate::envelope::HEADER_SIZE)
2160 .saturating_add(RESPONSE_BUFFER_TRAILER_SLACK);
2161
2162 loop {
2163 match std::future::poll_fn(|cx| body.as_mut().poll_frame(cx)).await {
2164 Some(Ok(frame)) => {
2165 if frame.is_data() {
2166 if let Ok(data) = frame.into_data() {
2167 if !data.is_empty() {
2168 has_body_data = true;
2169 }
2170 let remaining = max_buf_size.saturating_sub(buf.len());
2171 let append_len = data.len().min(remaining);
2172 buf.extend_from_slice(&data[..append_len]);
2173 if matches!(config.protocol, Protocol::GrpcWeb)
2174 && let Some(trailer_end) = grpc_web_trailer_frame_end(&buf)
2175 {
2176 buf.truncate(trailer_end);
2177 break;
2178 }
2179 if append_len < data.len() {
2180 return Err(ConnectError::resource_exhausted(format!(
2181 "response body size exceeds limit {max_buf_size}"
2182 )));
2183 }
2184 }
2185 } else if frame.is_trailers()
2186 && let Ok(trailers) = frame.into_trailers()
2187 {
2188 grpc_trailers = trailers;
2189 }
2190 }
2191 Some(Err(e)) => {
2192 return Err(classify_body_read_error(
2193 "failed to read response body",
2194 &e,
2195 deadline,
2196 ));
2197 }
2198 None => break,
2199 }
2200 }
2201
2202 let mut message_data: Option<Bytes> = None;
2208 let mut message_count = 0u32;
2209
2210 while !buf.is_empty() {
2211 if buf[0] & 0x80 != 0 {
2213 let decompression = response_encoding
2214 .as_deref()
2215 .map(|enc| (&config.compression, enc));
2216 if let Some(trailers) =
2217 parse_grpc_web_trailer_frame_with_compression(&buf, decompression)
2218 {
2219 grpc_trailers = trailers;
2220 }
2221 break;
2222 }
2223
2224 let grpc_max_msg = options
2225 .max_message_size
2226 .unwrap_or(crate::service::DEFAULT_MAX_MESSAGE_SIZE);
2227
2228 let envelope = match Envelope::decode_with_limit(&mut buf, grpc_max_msg) {
2229 Ok(Some(env)) => env,
2230 Ok(None) => break,
2231 Err(e) => {
2232 return Err(ConnectError::internal(format!(
2233 "envelope decode failed: {e}"
2234 )));
2235 }
2236 };
2237
2238 if message_count > 0 {
2239 let mut err = ConnectError::unimplemented(
2240 "received multiple response messages where exactly one was expected",
2241 );
2242 err.set_response_headers(resp_headers);
2243 return Err(err);
2244 }
2245
2246 let data = if envelope.is_compressed() {
2247 let enc = response_encoding.as_deref().ok_or_else(|| {
2248 ConnectError::internal("received compressed message without grpc-encoding header")
2249 })?;
2250 if enc == "identity" {
2251 return Err(ConnectError::internal(
2252 "received compressed message with identity encoding",
2253 ));
2254 }
2255 config
2256 .compression
2257 .decompress_with_limit(enc, envelope.data, grpc_max_msg)
2258 .map_err(map_response_decompression_error)?
2259 } else {
2260 envelope.data
2261 };
2262
2263 message_count += 1;
2264 message_data = Some(data);
2265 }
2266
2267 let effective_trailers = if !grpc_trailers.is_empty() {
2271 &grpc_trailers
2272 } else if !has_body_data {
2273 &resp_headers
2275 } else {
2276 &grpc_trailers };
2278
2279 if let Some(mut err) = parse_grpc_error_from_trailers(effective_trailers) {
2280 err.set_response_headers(resp_headers);
2281 return Err(err);
2282 }
2283
2284 if effective_trailers.get("grpc-status").is_none() {
2289 let mut err = if deadline_elapsed(deadline) {
2290 ConnectError::deadline_exceeded("request timeout")
2293 } else {
2294 ConnectError::internal("gRPC response missing grpc-status trailer")
2295 };
2296 err.set_response_headers(resp_headers);
2297 return Err(err);
2298 }
2299
2300 let data = match message_data {
2301 Some(data) => data,
2302 None => {
2303 let mut err = ConnectError::unimplemented("gRPC response contained no message data");
2305 err.set_response_headers(resp_headers);
2306 return Err(err);
2307 }
2308 };
2309
2310 if let Some(max_size) = options.max_message_size
2311 && data.len() > max_size
2312 {
2313 return Err(ConnectError::new(
2314 ErrorCode::ResourceExhausted,
2315 format!("message size {} exceeds limit {}", data.len(), max_size),
2316 ));
2317 }
2318
2319 let message = decode_response_view::<RespView>(data, config.codec_format)?;
2320
2321 Ok(UnaryResponse {
2322 headers: resp_headers,
2323 body: message,
2324 trailers: grpc_trailers,
2325 })
2326}
2327
2328fn validate_grpc_response_content_type(
2342 resp_headers: &http::HeaderMap,
2343 config: &ClientConfig,
2344) -> Result<(), ConnectError> {
2345 debug_assert!(
2346 matches!(config.protocol, Protocol::Grpc | Protocol::GrpcWeb),
2347 "gRPC response content-type validation is only for gRPC/gRPC-Web"
2348 );
2349
2350 let Some(resp_content_type) = resp_headers.get(http::header::CONTENT_TYPE) else {
2351 return Ok(());
2352 };
2353
2354 let ct = resp_content_type.to_str().unwrap_or("");
2355 let ct_normalized = ct
2356 .split_once(';')
2357 .map_or(ct, |(media_type, _params)| media_type)
2358 .trim();
2359 let expected = config
2360 .protocol
2361 .response_content_type(config.codec_format, false);
2362 let (bare, family_prefix) = match config.protocol {
2363 Protocol::Grpc => ("application/grpc", "application/grpc+"),
2364 Protocol::GrpcWeb => ("application/grpc-web", "application/grpc-web+"),
2365 Protocol::Connect => return Ok(()),
2368 };
2369
2370 if ct_normalized == expected || ct_normalized == bare {
2371 return Ok(());
2372 }
2373
2374 let code = if ct_normalized.starts_with(family_prefix) {
2375 ErrorCode::Internal
2376 } else {
2377 ErrorCode::Unknown
2378 };
2379 let mut err = ConnectError::new(
2380 code,
2381 format!("unexpected content-type: {ct} (expected {expected})"),
2382 );
2383 err.set_response_headers(resp_headers.clone());
2384 Err(err)
2385}
2386
2387#[derive(Debug)]
2393struct StreamEnd {
2394 outcome: Result<(), ConnectError>,
2396 trailers: Option<http::HeaderMap>,
2397}
2398
2399impl StreamEnd {
2400 fn attach_metadata(&mut self, headers: &http::HeaderMap) {
2411 if let Err(e) = &mut self.outcome {
2412 e.set_response_headers(headers.clone());
2413 if let Some(trailers) = &self.trailers {
2414 e.set_trailers(error_metadata_from_trailers(trailers));
2415 }
2416 }
2417 }
2418
2419 fn replay<T>(&self) -> Result<Option<T>, ConnectError> {
2420 match &self.outcome {
2421 Ok(()) => Ok(None),
2422 Err(e) => Err(e.clone()),
2423 }
2424 }
2425}
2426
2427impl From<ConnectError> for StreamEnd {
2432 fn from(e: ConnectError) -> Self {
2433 StreamEnd {
2434 outcome: Err(e),
2435 trailers: None,
2436 }
2437 }
2438}
2439
2440enum BodyPoll {
2442 Data,
2444 Trailers(http::HeaderMap),
2446 Eof,
2448}
2449
2450pub struct ServerStream<B, RespView> {
2471 headers: http::HeaderMap,
2472 body: B,
2473 buf: BytesMut,
2474 encoding: Option<String>,
2475 compression: CompressionRegistry,
2476 codec_format: CodecFormat,
2477 protocol: Protocol,
2478 max_message_size: Option<usize>,
2479 deadline: Option<std::time::Instant>,
2480 end: Option<StreamEnd>,
2482 saw_body_data: bool,
2486 _phantom: PhantomData<RespView>,
2487}
2488
2489impl<B, RespView> std::fmt::Debug for ServerStream<B, RespView> {
2493 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
2494 f.debug_struct("ServerStream")
2495 .field("protocol", &self.protocol)
2496 .field("codec_format", &self.codec_format)
2497 .field("encoding", &self.encoding)
2498 .field("ended", &self.end.is_some())
2499 .field(
2500 "error",
2501 &self.end.as_ref().and_then(|e| e.outcome.as_ref().err()),
2502 )
2503 .field(
2504 "has_trailers",
2505 &self.end.as_ref().is_some_and(|e| e.trailers.is_some()),
2506 )
2507 .field("buffered_bytes", &self.buf.len())
2508 .finish_non_exhaustive()
2509 }
2510}
2511
2512impl<B, RespView> ServerStream<B, RespView>
2513where
2514 B: Body<Data = Bytes> + Unpin,
2515 B::Error: std::fmt::Display,
2516 RespView: MessageView<'static> + Send,
2517 RespView::Owned: buffa::Message + crate::codec::JsonDeserialize,
2518{
2519 #[must_use]
2521 pub fn headers(&self) -> &http::HeaderMap {
2522 &self.headers
2523 }
2524
2525 pub async fn message<M>(&mut self) -> Result<Option<crate::StreamMessage<M>>, ConnectError>
2565 where
2566 RespView: MessageView<'static, Owned = M>,
2572 M: HasMessageView<View<'static> = RespView>,
2573 {
2574 if let Some(end) = &self.end {
2577 return end.replay();
2578 }
2579 match self.next_message_or_end().await {
2580 Ok(msg) => Ok(Some(crate::StreamMessage::from_owned_view(msg))),
2581 Err(mut end) => {
2585 debug_assert!(self.end.is_none(), "terminal record written twice");
2586 end.attach_metadata(&self.headers);
2587 self.end.get_or_insert(end).replay()
2588 }
2589 }
2590 }
2591
2592 async fn next_message_or_end(&mut self) -> Result<OwnedView<RespView>, StreamEnd> {
2599 loop {
2600 if matches!(self.protocol, Protocol::GrpcWeb)
2604 && self.buf.len() >= 5
2605 && self.buf[0] & 0x80 != 0
2606 {
2607 let trailer_len =
2608 u32::from_be_bytes([self.buf[1], self.buf[2], self.buf[3], self.buf[4]])
2609 as usize;
2610 if self.buf.len() >= trailer_len.saturating_add(5) {
2617 let decompression =
2621 self.encoding.as_deref().map(|enc| (&self.compression, enc));
2622 let parsed =
2623 parse_grpc_web_trailer_frame_with_compression(&self.buf, decompression);
2624 return Err(self.classify_grpc_end(parsed));
2625 }
2626 }
2629
2630 let envelope_result = if matches!(self.protocol, Protocol::GrpcWeb)
2634 && !self.buf.is_empty()
2635 && self.buf[0] & 0x80 != 0
2636 {
2637 None
2640 } else {
2641 Envelope::decode_with_limit(
2642 &mut self.buf,
2643 self.max_message_size
2644 .unwrap_or(crate::service::DEFAULT_MAX_MESSAGE_SIZE),
2645 )?
2646 };
2647
2648 match envelope_result {
2649 Some(envelope) => {
2650 if envelope.is_end_stream() {
2651 return Err(self.process_end_stream(envelope));
2653 }
2654
2655 let data = self.decompress_envelope(envelope)?;
2657
2658 if let Some(max_size) = self.max_message_size
2660 && data.len() > max_size
2661 {
2662 return Err(ConnectError::new(
2663 ErrorCode::ResourceExhausted,
2664 format!("message size {} exceeds limit {}", data.len(), max_size),
2665 )
2666 .into());
2667 }
2668
2669 let msg = decode_response_view::<RespView>(data, self.codec_format)?;
2670 return Ok(msg);
2671 }
2672 None => match self.poll_body().await? {
2673 BodyPoll::Data => {} BodyPoll::Trailers(trailers) => {
2675 return Err(self.classify_grpc_end(Some(trailers)));
2676 }
2677 BodyPoll::Eof => {
2678 if matches!(self.protocol, Protocol::Connect) {
2679 return Err(ConnectError::internal(
2685 "Connect streaming response ended without END_STREAM envelope",
2686 )
2687 .into());
2688 }
2689 let parsed = if matches!(self.protocol, Protocol::GrpcWeb)
2698 && !self.buf.is_empty()
2699 && self.buf[0] & 0x80 != 0
2700 {
2701 let decompression =
2702 self.encoding.as_deref().map(|enc| (&self.compression, enc));
2703 parse_grpc_web_trailer_frame_with_compression(&self.buf, decompression)
2704 } else {
2705 None
2706 };
2707 return Err(self.classify_grpc_end(parsed));
2708 }
2709 },
2710 }
2711 }
2712 }
2713
2714 fn classify_grpc_end(&self, trailers: Option<http::HeaderMap>) -> StreamEnd {
2732 debug_assert!(
2733 matches!(self.protocol, Protocol::Grpc | Protocol::GrpcWeb),
2734 "Connect ends classify in process_end_stream / the missing-END_STREAM arm"
2735 );
2736 let outcome = match trailers.as_ref().and_then(parse_grpc_error_from_trailers) {
2737 Some(err) => Err(err),
2740 None => {
2741 let has_status = |h: &http::HeaderMap| h.contains_key("grpc-status");
2742 let trailers_only = !self.saw_body_data;
2743 if trailers.as_ref().is_some_and(has_status)
2744 || (trailers_only && has_status(&self.headers))
2745 {
2746 Ok(())
2747 } else if deadline_elapsed(self.deadline) {
2748 Err(ConnectError::deadline_exceeded("request timeout"))
2749 } else if trailers.is_some() {
2750 Err(ConnectError::new(
2751 ErrorCode::Unknown,
2752 "protocol error: grpc-status missing from trailers",
2753 ))
2754 } else {
2755 Err(ConnectError::internal("stream ended without grpc-status"))
2756 }
2757 }
2758 };
2759 StreamEnd { outcome, trailers }
2760 }
2761
2762 #[must_use]
2768 pub fn trailers(&self) -> Option<&http::HeaderMap> {
2769 self.end.as_ref().and_then(|e| e.trailers.as_ref())
2770 }
2771
2772 #[must_use]
2780 pub fn error(&self) -> Option<&ConnectError> {
2781 self.end.as_ref().and_then(|e| e.outcome.as_ref().err())
2782 }
2783
2784 async fn poll_body(&mut self) -> Result<BodyPoll, ConnectError> {
2793 let max_buf_size = self
2797 .max_message_size
2798 .unwrap_or(crate::service::DEFAULT_MAX_MESSAGE_SIZE)
2799 .saturating_add(2 * crate::envelope::HEADER_SIZE)
2800 .saturating_add(RESPONSE_BUFFER_TRAILER_SLACK);
2801
2802 loop {
2803 let deadline = self.deadline;
2813 let frame = with_deadline(deadline, async {
2814 Ok(Pin::new(&mut self.body).frame().await)
2815 })
2816 .await?;
2817
2818 match frame {
2819 None => return Ok(BodyPoll::Eof),
2820 Some(Ok(frame)) => {
2821 if frame.is_data() {
2822 if let Ok(data) = frame.into_data() {
2823 if !data.is_empty() {
2824 self.saw_body_data = true;
2825 }
2826 if self.buf.len().saturating_add(data.len()) > max_buf_size {
2827 return Err(ConnectError::resource_exhausted(format!(
2828 "response buffer exceeds limit {max_buf_size}"
2829 )));
2830 }
2831 self.buf.extend_from_slice(&data);
2832 return Ok(BodyPoll::Data);
2833 }
2834 } else if frame.is_trailers()
2835 && let Ok(trailers) = frame.into_trailers()
2836 && matches!(self.protocol, Protocol::Grpc | Protocol::GrpcWeb)
2837 {
2838 return Ok(BodyPoll::Trailers(trailers));
2842 }
2843 }
2844 Some(Err(e)) => {
2845 return Err(classify_body_read_error(
2846 "failed to read response body",
2847 &e,
2848 deadline,
2849 ));
2850 }
2851 }
2852 }
2853 }
2854
2855 fn decompress_envelope(&self, envelope: Envelope) -> Result<Bytes, ConnectError> {
2857 if envelope.is_compressed() {
2858 let encoding = self.encoding.as_deref().ok_or_else(|| {
2859 ConnectError::internal(
2860 "received compressed message without content-encoding header",
2861 )
2862 })?;
2863 let max_size = self
2864 .max_message_size
2865 .unwrap_or(crate::service::DEFAULT_MAX_MESSAGE_SIZE);
2866 self.compression
2867 .decompress_with_limit(encoding, envelope.data, max_size)
2868 .map_err(map_response_decompression_error)
2869 } else {
2870 Ok(envelope.data)
2871 }
2872 }
2873
2874 fn process_end_stream(&self, envelope: Envelope) -> StreamEnd {
2876 let end_stream_data = match self.decompress_envelope(envelope) {
2877 Ok(data) => data,
2878 Err(e) => return e.into(),
2879 };
2880
2881 let end_stream = match parse_connect_end_stream(&end_stream_data) {
2882 Ok(end_stream) => end_stream,
2883 Err(e) => return e.into(),
2884 };
2885
2886 let trailers = end_stream.metadata.map(|metadata| {
2887 let mut trailers = http::HeaderMap::new();
2888 append_metadata_capped(&mut trailers, metadata);
2889 trailers
2890 });
2891
2892 let outcome = match end_stream.error {
2893 Some(err) => Err(end_stream_error_to_connect_error(err)),
2894 None => Ok(()),
2895 };
2896
2897 StreamEnd { outcome, trailers }
2898 }
2899}
2900
2901pub async fn call_server_stream<T, Req, RespView>(
2922 transport: &T,
2923 config: &ClientConfig,
2924 service: &str,
2925 method: &str,
2926 request: Req,
2927 options: CallOptions,
2928) -> Result<ServerStream<T::ResponseBody, RespView>, ConnectError>
2929where
2930 T: ClientTransport,
2931 <T::ResponseBody as Body>::Error: std::fmt::Display,
2932 Req: buffa::Message + crate::codec::JsonSerialize,
2933 RespView: MessageView<'static> + Send,
2934 RespView::Owned: buffa::Message + crate::codec::JsonDeserialize,
2935{
2936 let options = effective_options(config, options);
2937
2938 let base_str = config.base_uri.to_string();
2940 let base_str = base_str.trim_end_matches('/');
2941 let full_uri = format!("{base_str}/{service}/{method}");
2942 let uri: Uri = full_uri
2943 .parse()
2944 .map_err(|e| ConnectError::internal(format!("invalid URI: {e}")))?;
2945
2946 let body = match config.codec_format {
2948 CodecFormat::Proto => request.encode_to_bytes(),
2949 CodecFormat::Json => encode_json(&request)?,
2950 };
2951
2952 let compression_for_encoder = config.request_compression.as_ref().map(|enc| {
2955 (
2956 std::sync::Arc::new(config.compression.clone()),
2957 enc.as_str(),
2958 )
2959 });
2960 let mut encoder = crate::envelope::EnvelopeEncoder::new(
2961 compression_for_encoder,
2962 config.compression_policy.with_override(options.compress),
2963 );
2964 let mut request_buf = bytes::BytesMut::new();
2965 tokio_util::codec::Encoder::encode(&mut encoder, body, &mut request_buf)?;
2966 let request_body = request_buf.freeze();
2967
2968 let deadline = client_deadline(options.timeout, config.protocol);
2970
2971 let mut builder = Request::builder().method(http::Method::POST).uri(uri);
2973 builder = add_streaming_request_headers(builder, config, options.timeout);
2974
2975 let headers = builder.headers_mut().unwrap();
2977 for (name, value) in &options.headers {
2978 headers.append(name.clone(), value.clone());
2979 }
2980
2981 let http_request = builder
2982 .body(full_body(request_body))
2983 .map_err(|e| ConnectError::internal(format!("failed to build request: {e}")))?;
2984
2985 with_deadline(deadline, async {
2989 let response = transport
2990 .send(http_request)
2991 .await
2992 .map_err(|e| map_transport_send_error(e, "request failed"))?;
2993
2994 make_server_stream(
2995 response,
2996 config.protocol,
2997 &config.compression,
2998 config.codec_format,
2999 options.max_message_size,
3000 deadline,
3001 )
3002 .await
3003 })
3004 .await
3005}
3006
3007async fn make_server_stream<B, RespView>(
3018 response: Response<B>,
3019 protocol: Protocol,
3020 compression: &CompressionRegistry,
3021 codec_format: CodecFormat,
3022 max_message_size: Option<usize>,
3023 deadline: Option<std::time::Instant>,
3024) -> Result<ServerStream<B, RespView>, ConnectError>
3025where
3026 B: Body<Data = Bytes> + Send,
3027 B::Error: std::fmt::Display,
3028 RespView: MessageView<'static> + Send,
3029 RespView::Owned: buffa::Message + crate::codec::JsonDeserialize,
3030{
3031 let response_headers = response.headers().clone();
3032 let status = response.status();
3033
3034 if matches!(protocol, Protocol::Grpc | Protocol::GrpcWeb)
3036 && let Some(mut err) = parse_grpc_error_from_trailers(&response_headers)
3037 {
3038 err.set_response_headers(response_headers);
3039 return Err(err);
3040 }
3041
3042 if !status.is_success() {
3044 if matches!(protocol, Protocol::Connect) {
3045 let error_encoding = response_headers
3046 .get(http::header::CONTENT_ENCODING)
3047 .and_then(|v| v.to_str().ok())
3048 .map(|s| s.to_owned());
3049
3050 let stream_max_err_size =
3051 max_message_size.unwrap_or(crate::service::DEFAULT_MAX_MESSAGE_SIZE);
3052
3053 let body = collect_body_bounded(response.into_body(), stream_max_err_size, deadline)
3058 .await
3059 .map_err(|mut e| {
3060 e.set_response_headers(response_headers.clone());
3061 e
3062 })?;
3063
3064 let body = match error_encoding {
3067 Some(encoding) => {
3068 match compression.decompress_with_limit(&encoding, body, stream_max_err_size) {
3069 Ok(decompressed) => Some(decompressed),
3070 Err(e) => {
3071 tracing::debug!(
3072 "failed to decompress Connect error response ({encoding}): {e}"
3073 );
3074 None
3075 }
3076 }
3077 }
3078 None => Some(body),
3079 };
3080
3081 if let Some(body) = body
3082 && let Ok(error) = serde_json::from_slice::<ConnectErrorResponse>(&body)
3083 {
3084 let code = error
3085 .code
3086 .as_deref()
3087 .and_then(|s| s.parse::<ErrorCode>().ok())
3088 .unwrap_or_else(|| http_status_to_error_code(status));
3089 let mut err = ConnectError::new(code, error.message.unwrap_or_default());
3090 err.details = error.details;
3091 err.set_response_headers(response_headers);
3092 return Err(err);
3093 }
3094 }
3095
3096 let code = http_status_to_error_code(status);
3097 let mut err = ConnectError::new(code, format!("HTTP error {}", status.as_u16()));
3098 err.set_response_headers(response_headers);
3099 return Err(err);
3100 }
3101
3102 let encoding = response_headers
3104 .get(protocol.content_encoding_header())
3105 .and_then(|v| v.to_str().ok())
3106 .map(|s| s.to_owned());
3107
3108 Ok(ServerStream {
3109 headers: response_headers,
3110 body: response.into_body(),
3111 buf: BytesMut::new(),
3112 encoding,
3113 compression: compression.clone(),
3114 codec_format,
3115 protocol,
3116 max_message_size,
3117 deadline,
3118 end: None,
3119 saw_body_data: false,
3120 _phantom: PhantomData,
3121 })
3122}
3123
3124struct ChannelBody {
3135 rx: tokio::sync::mpsc::Receiver<Result<Bytes, ConnectError>>,
3136}
3137
3138impl Body for ChannelBody {
3139 type Data = Bytes;
3140 type Error = ConnectError;
3141
3142 fn poll_frame(
3143 mut self: Pin<&mut Self>,
3144 cx: &mut std::task::Context<'_>,
3145 ) -> std::task::Poll<Option<Result<http_body::Frame<Bytes>, ConnectError>>> {
3146 self.rx
3147 .poll_recv(cx)
3148 .map(|opt| opt.map(|r| r.map(http_body::Frame::data)))
3149 }
3150}
3151
3152#[pin_project::pin_project]
3167struct EncodingBody<S> {
3168 #[pin]
3169 stream: sync_wrapper::SyncWrapper<S>,
3170 encoder: crate::envelope::EnvelopeEncoder,
3171 codec_format: CodecFormat,
3172 error: std::sync::Arc<std::sync::Mutex<Option<ConnectError>>>,
3175 done: bool,
3178}
3179
3180impl<S, Req> Body for EncodingBody<S>
3181where
3182 S: Stream<Item = Req>,
3183 Req: buffa::Message + crate::codec::JsonSerialize,
3184{
3185 type Data = Bytes;
3186 type Error = ConnectError;
3187
3188 fn poll_frame(
3189 self: Pin<&mut Self>,
3190 cx: &mut std::task::Context<'_>,
3191 ) -> std::task::Poll<Option<Result<http_body::Frame<Bytes>, ConnectError>>> {
3192 use std::task::Poll;
3193
3194 let this = self.project();
3195 if *this.done {
3196 return Poll::Ready(None);
3197 }
3198
3199 let Some(request) = std::task::ready!(this.stream.get_pin_mut().poll_next(cx)) else {
3200 *this.done = true;
3203 return Poll::Ready(None);
3204 };
3205
3206 let mut record_error = |err: &ConnectError| {
3207 *this.done = true;
3208 *this
3209 .error
3210 .lock()
3211 .unwrap_or_else(std::sync::PoisonError::into_inner) = Some(err.clone());
3212 };
3213
3214 let msg_bytes = match this.codec_format {
3215 CodecFormat::Proto => request.encode_to_bytes(),
3216 CodecFormat::Json => match encode_json(&request) {
3217 Ok(bytes) => bytes,
3218 Err(err) => {
3219 record_error(&err);
3220 return Poll::Ready(Some(Err(err)));
3221 }
3222 },
3223 };
3224
3225 let mut envelope_buf = BytesMut::new();
3226 match tokio_util::codec::Encoder::encode(this.encoder, msg_bytes, &mut envelope_buf) {
3227 Ok(()) => Poll::Ready(Some(Ok(http_body::Frame::data(envelope_buf.freeze())))),
3228 Err(err) => {
3229 record_error(&err);
3230 Poll::Ready(Some(Err(err)))
3231 }
3232 }
3233 }
3234}
3235
3236enum RecvState<B, RespView> {
3256 AwaitingHeaders(tokio::task::JoinHandle<Result<Response<B>, ConnectError>>),
3260 Constructing(tokio::task::JoinHandle<Result<Box<ServerStream<B, RespView>>, ConnectError>>),
3262 Ready(Box<ServerStream<B, RespView>>),
3264 Failed(ConnectError),
3266}
3267
3268pub struct BidiStream<B, Req, RespView> {
3308 send: BidiSendHalf<Req>,
3314 recv: BidiRecvHalf<B, RespView>,
3315}
3316
3317impl<B, Req, RespView> std::fmt::Debug for BidiStream<B, Req, RespView> {
3319 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
3320 f.debug_struct("BidiStream")
3321 .field("send", &self.send)
3322 .field("recv", &self.recv)
3323 .finish()
3324 }
3325}
3326
3327pub struct BidiSendHalf<Req> {
3336 tx: Option<tokio::sync::mpsc::Sender<Result<Bytes, ConnectError>>>,
3337 encoder: crate::envelope::EnvelopeEncoder,
3338 codec_format: CodecFormat,
3339 deadline: Option<std::time::Instant>,
3341 _req: PhantomData<Req>,
3342}
3343
3344impl<Req> std::fmt::Debug for BidiSendHalf<Req> {
3345 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
3346 f.debug_struct("BidiSendHalf")
3347 .field("send_closed", &self.tx.is_none())
3348 .field("codec_format", &self.codec_format)
3349 .finish_non_exhaustive()
3350 }
3351}
3352
3353pub struct BidiRecvHalf<B, RespView> {
3363 recv: RecvState<B, RespView>,
3365 stream_config: StreamConfig,
3368}
3369
3370impl<B, RespView> std::fmt::Debug for BidiRecvHalf<B, RespView> {
3371 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
3372 let (recv_state, recv_error) = match &self.recv {
3373 RecvState::AwaitingHeaders(_) => ("AwaitingHeaders", None),
3374 RecvState::Constructing(_) => ("Constructing", None),
3375 RecvState::Ready(_) => ("Ready", None),
3376 RecvState::Failed(err) => ("Failed", Some(err)),
3377 };
3378 f.debug_struct("BidiRecvHalf")
3379 .field("recv_state", &recv_state)
3380 .field("protocol", &self.stream_config.protocol)
3381 .field("codec_format", &self.stream_config.codec_format)
3382 .field("recv_error", &recv_error)
3383 .finish_non_exhaustive()
3384 }
3385}
3386
3387impl<B, RespView> Drop for BidiRecvHalf<B, RespView> {
3395 fn drop(&mut self) {
3396 match &self.recv {
3397 RecvState::AwaitingHeaders(task) => task.abort(),
3398 RecvState::Constructing(task) => task.abort(),
3399 RecvState::Ready(_) | RecvState::Failed(_) => {}
3400 }
3401 }
3402}
3403
3404#[derive(Debug)]
3407struct StreamConfig {
3408 protocol: Protocol,
3409 codec_format: CodecFormat,
3410 compression: CompressionRegistry,
3411 max_message_size: Option<usize>,
3412 deadline: Option<std::time::Instant>,
3413}
3414
3415impl<Req> BidiSendHalf<Req>
3416where
3417 Req: buffa::Message + crate::codec::JsonSerialize,
3418{
3419 pub async fn send(&mut self, msg: Req) -> Result<(), ConnectError> {
3430 if let Some(d) = self.deadline
3433 && std::time::Instant::now() >= d
3434 {
3435 return Err(ConnectError::deadline_exceeded(
3436 "client-side deadline exceeded",
3437 ));
3438 }
3439
3440 let Some(tx) = &self.tx else {
3441 return Err(ConnectError::internal("send after close_send"));
3442 };
3443
3444 let msg_bytes = match self.codec_format {
3447 CodecFormat::Proto => msg.encode_to_bytes(),
3448 CodecFormat::Json => encode_json(&msg)?,
3449 };
3450
3451 let mut envelope_buf = BytesMut::new();
3452 tokio_util::codec::Encoder::encode(&mut self.encoder, msg_bytes, &mut envelope_buf)?;
3453
3454 tx.send(Ok(envelope_buf.freeze())).await.map_err(|_| {
3455 ConnectError::unavailable("stream closed by server (call message() for error)")
3459 })
3460 }
3461
3462 pub fn close_send(&mut self) {
3468 self.tx = None; }
3470}
3471
3472impl<B, RespView> BidiRecvHalf<B, RespView>
3473where
3474 B: Body<Data = Bytes> + Send + Unpin,
3475 B::Error: std::fmt::Display,
3476 RespView: MessageView<'static> + Send,
3477 RespView::Owned: buffa::Message + crate::codec::JsonDeserialize,
3478{
3479 pub async fn message<M>(&mut self) -> Result<Option<crate::StreamMessage<M>>, ConnectError>
3495 where
3496 B: 'static,
3501 RespView: MessageView<'static, Owned = M> + 'static,
3502 M: HasMessageView<View<'static> = RespView>,
3503 {
3504 loop {
3505 match &mut self.recv {
3506 RecvState::AwaitingHeaders(task) => {
3507 let response = match with_deadline(self.stream_config.deadline, async {
3512 (&mut *task).await.map_err(|e| {
3515 ConnectError::internal(format!("transport send task failed: {e}"))
3518 })?
3519 })
3520 .await
3521 {
3522 Ok(response) => response,
3523 Err(e) => {
3524 task.abort();
3529 self.recv = RecvState::Failed(e.clone());
3530 return Err(e);
3531 }
3532 };
3533
3534 let protocol = self.stream_config.protocol;
3535 let codec_format = self.stream_config.codec_format;
3536 let compression = self.stream_config.compression.clone();
3537 let max_message_size = self.stream_config.max_message_size;
3538 let deadline = self.stream_config.deadline;
3539
3540 let construct_task = tokio::spawn(async move {
3541 let stream = with_deadline(
3545 deadline,
3546 make_server_stream(
3547 response,
3548 protocol,
3549 &compression,
3550 codec_format,
3551 max_message_size,
3552 deadline,
3553 ),
3554 )
3555 .await?;
3556
3557 Ok(Box::new(stream))
3558 });
3559
3560 self.recv = RecvState::Constructing(construct_task);
3561 }
3562 RecvState::Constructing(task) => {
3563 let result = match task.await {
3567 Ok(result) => result,
3568 Err(e) => Err(ConnectError::internal(format!(
3569 "response stream construction task failed: {e}"
3570 ))),
3571 };
3572
3573 match result {
3574 Ok(stream) => self.recv = RecvState::Ready(stream),
3575 Err(e) => {
3576 self.recv = RecvState::Failed(e.clone());
3577 return Err(e);
3578 }
3579 }
3580 }
3581 RecvState::Ready(stream) => return stream.message().await,
3582 RecvState::Failed(e) => return Err(e.clone()),
3583 }
3584 }
3585 }
3586
3587 #[must_use]
3591 pub fn headers(&self) -> Option<&http::HeaderMap> {
3592 match &self.recv {
3593 RecvState::Ready(s) => Some(s.headers()),
3594 _ => None,
3595 }
3596 }
3597
3598 #[must_use]
3601 pub fn trailers(&self) -> Option<&http::HeaderMap> {
3602 match &self.recv {
3603 RecvState::Ready(s) => s.trailers(),
3604 _ => None,
3605 }
3606 }
3607
3608 #[must_use]
3618 pub fn error(&self) -> Option<&ConnectError> {
3619 match &self.recv {
3620 RecvState::Ready(s) => s.error(),
3621 RecvState::Failed(e) => Some(e),
3622 _ => None,
3623 }
3624 }
3625}
3626
3627impl<B, Req, RespView> BidiStream<B, Req, RespView> {
3628 #[must_use]
3670 pub fn into_split(self) -> (BidiSendHalf<Req>, BidiRecvHalf<B, RespView>) {
3671 (self.send, self.recv)
3672 }
3673}
3674
3675impl<B, Req, RespView> BidiStream<B, Req, RespView>
3676where
3677 B: Body<Data = Bytes> + Send + Unpin,
3678 B::Error: std::fmt::Display,
3679 Req: buffa::Message + crate::codec::JsonSerialize,
3680 RespView: MessageView<'static> + Send,
3681 RespView::Owned: buffa::Message + crate::codec::JsonDeserialize,
3682{
3683 pub async fn send(&mut self, msg: Req) -> Result<(), ConnectError> {
3689 self.send.send(msg).await
3690 }
3691
3692 pub fn close_send(&mut self) {
3695 self.send.close_send();
3696 }
3697
3698 pub async fn message<M>(&mut self) -> Result<Option<crate::StreamMessage<M>>, ConnectError>
3704 where
3705 B: 'static,
3706 RespView: MessageView<'static, Owned = M> + 'static,
3707 M: HasMessageView<View<'static> = RespView>,
3708 {
3709 self.recv.message().await
3710 }
3711
3712 #[must_use]
3714 pub fn headers(&self) -> Option<&http::HeaderMap> {
3715 self.recv.headers()
3716 }
3717
3718 #[must_use]
3720 pub fn trailers(&self) -> Option<&http::HeaderMap> {
3721 self.recv.trailers()
3722 }
3723
3724 #[must_use]
3727 pub fn error(&self) -> Option<&ConnectError> {
3728 self.recv.error()
3729 }
3730}
3731
3732pub async fn call_bidi_stream<T, Req, RespView>(
3753 transport: &T,
3754 config: &ClientConfig,
3755 service: &str,
3756 method: &str,
3757 options: CallOptions,
3758) -> Result<BidiStream<T::ResponseBody, Req, RespView>, ConnectError>
3759where
3760 T: ClientTransport,
3761 <T::ResponseBody as Body>::Error: std::fmt::Display,
3762 Req: buffa::Message + crate::codec::JsonSerialize,
3763 RespView: MessageView<'static> + Send,
3764 RespView::Owned: buffa::Message + crate::codec::JsonDeserialize,
3765{
3766 let options = effective_options(config, options);
3767
3768 let base_str = config.base_uri.to_string();
3770 let base_str = base_str.trim_end_matches('/');
3771 let full_uri = format!("{base_str}/{service}/{method}");
3772 let uri: Uri = full_uri
3773 .parse()
3774 .map_err(|e| ConnectError::internal(format!("invalid URI: {e}")))?;
3775
3776 let (tx, rx) = tokio::sync::mpsc::channel::<Result<Bytes, ConnectError>>(32);
3779 let body: ClientBody = ChannelBody { rx }.boxed();
3780
3781 let compression_for_encoder = config.request_compression.as_ref().map(|enc| {
3783 (
3784 std::sync::Arc::new(config.compression.clone()),
3785 enc.as_str(),
3786 )
3787 });
3788 let encoder = crate::envelope::EnvelopeEncoder::new(
3789 compression_for_encoder,
3790 config.compression_policy.with_override(options.compress),
3791 );
3792
3793 let deadline = client_deadline(options.timeout, config.protocol);
3794
3795 let mut builder = Request::builder().method(http::Method::POST).uri(uri);
3797 builder = add_streaming_request_headers(builder, config, options.timeout);
3798
3799 let headers = builder.headers_mut().unwrap();
3800 for (name, value) in &options.headers {
3801 headers.append(name.clone(), value.clone());
3802 }
3803
3804 let http_request = builder
3805 .body(body)
3806 .map_err(|e| ConnectError::internal(format!("failed to build request: {e}")))?;
3807
3808 let response_fut = transport.send(http_request);
3819 let response_task = tokio::spawn(async move {
3820 response_fut
3821 .await
3822 .map_err(|e| map_transport_send_error(e, "request failed"))
3823 });
3824
3825 Ok(BidiStream {
3826 send: BidiSendHalf {
3827 tx: Some(tx),
3828 encoder,
3829 codec_format: config.codec_format,
3830 deadline,
3831 _req: PhantomData,
3832 },
3833 recv: BidiRecvHalf {
3834 recv: RecvState::AwaitingHeaders(response_task),
3835 stream_config: StreamConfig {
3836 protocol: config.protocol,
3837 codec_format: config.codec_format,
3838 compression: config.compression.clone(),
3839 max_message_size: options.max_message_size,
3840 deadline,
3841 },
3842 },
3843 })
3844}
3845
3846pub async fn call_client_stream<T, Req, RespView>(
3898 transport: &T,
3899 config: &ClientConfig,
3900 service: &str,
3901 method: &str,
3902 requests: impl ClientRequestStream<Req>,
3903 options: CallOptions,
3904) -> Result<UnaryResponse<OwnedView<RespView>>, ConnectError>
3905where
3906 T: ClientTransport,
3907 <T::ResponseBody as Body>::Error: std::fmt::Display,
3908 Req: buffa::Message + crate::codec::JsonSerialize,
3909 RespView: MessageView<'static> + Send,
3910 RespView::Owned: buffa::Message + crate::codec::JsonDeserialize,
3911{
3912 let options = effective_options(config, options);
3913
3914 let base_str = config.base_uri.to_string();
3916 let base_str = base_str.trim_end_matches('/');
3917 let full_uri = format!("{base_str}/{service}/{method}");
3918 let uri: Uri = full_uri
3919 .parse()
3920 .map_err(|e| ConnectError::internal(format!("invalid URI: {e}")))?;
3921
3922 let compression_for_encoder = config.request_compression.as_ref().map(|enc| {
3923 (
3924 std::sync::Arc::new(config.compression.clone()),
3925 enc.as_str(),
3926 )
3927 });
3928 let encoder = crate::envelope::EnvelopeEncoder::new(
3929 compression_for_encoder,
3930 config.compression_policy.with_override(options.compress),
3931 );
3932
3933 let encode_error: std::sync::Arc<std::sync::Mutex<Option<ConnectError>>> =
3938 std::sync::Arc::default();
3939 let body: ClientBody = EncodingBody {
3940 stream: sync_wrapper::SyncWrapper::new(requests),
3941 encoder,
3942 codec_format: config.codec_format,
3943 error: encode_error.clone(),
3944 done: false,
3945 }
3946 .boxed();
3947
3948 let deadline = client_deadline(options.timeout, config.protocol);
3950
3951 let mut builder = Request::builder().method(http::Method::POST).uri(uri);
3953 builder = add_streaming_request_headers(builder, config, options.timeout);
3954
3955 let headers = builder.headers_mut().unwrap();
3957 for (name, value) in &options.headers {
3958 headers.append(name.clone(), value.clone());
3959 }
3960
3961 let http_request = builder
3962 .body(body)
3963 .map_err(|e| ConnectError::internal(format!("failed to build request: {e}")))?;
3964
3965 let result = with_deadline(deadline, async {
3974 let response = transport
3975 .send(http_request)
3976 .await
3977 .map_err(|e| map_transport_send_error(e, "request failed"))?;
3978
3979 match config.protocol {
3982 Protocol::Grpc | Protocol::GrpcWeb => {
3983 parse_grpc_unary_response(response, config, &options, deadline).await
3984 }
3985 Protocol::Connect => {
3986 parse_connect_client_stream_response(response, config, &options, deadline).await
3987 }
3988 }
3989 })
3990 .await;
3991
3992 if let Some(err) = encode_error
4003 .lock()
4004 .unwrap_or_else(std::sync::PoisonError::into_inner)
4005 .take()
4006 {
4007 return Err(err);
4008 }
4009 result
4010}
4011
4012async fn parse_connect_client_stream_response<B, RespView>(
4014 response: Response<B>,
4015 config: &ClientConfig,
4016 options: &CallOptions,
4017 deadline: Option<std::time::Instant>,
4018) -> Result<UnaryResponse<OwnedView<RespView>>, ConnectError>
4019where
4020 B: Body<Data = Bytes> + Send,
4021 B::Error: std::fmt::Display,
4022 RespView: MessageView<'static> + Send,
4023 RespView::Owned: buffa::Message + crate::codec::JsonDeserialize,
4024{
4025 let status = response.status();
4026
4027 if !status.is_success() {
4028 let response_headers = response.headers().clone();
4029
4030 let error_encoding = response_headers
4031 .get(http::header::CONTENT_ENCODING)
4032 .and_then(|v| v.to_str().ok())
4033 .map(|s| s.to_owned());
4034
4035 let max_err_size = options
4036 .max_message_size
4037 .unwrap_or(crate::service::DEFAULT_MAX_MESSAGE_SIZE);
4038
4039 let body = collect_body_bounded(response.into_body(), max_err_size, deadline)
4043 .await
4044 .map_err(|mut e| {
4045 e.set_response_headers(response_headers.clone());
4046 e
4047 })?;
4048
4049 let body = match error_encoding {
4052 Some(encoding) => {
4053 match config
4054 .compression
4055 .decompress_with_limit(&encoding, body, max_err_size)
4056 {
4057 Ok(decompressed) => Some(decompressed),
4058 Err(e) => {
4059 tracing::debug!(
4060 "failed to decompress Connect error response ({encoding}): {e}"
4061 );
4062 None
4063 }
4064 }
4065 }
4066 None => Some(body),
4067 };
4068
4069 if let Some(body) = body
4070 && let Ok(error) = serde_json::from_slice::<ConnectErrorResponse>(&body)
4071 {
4072 let code = error
4073 .code
4074 .as_deref()
4075 .and_then(|s| s.parse::<ErrorCode>().ok())
4076 .unwrap_or_else(|| http_status_to_error_code(status));
4077 let mut err = ConnectError::new(code, error.message.unwrap_or_default());
4078 err.details = error.details;
4079 err.set_response_headers(response_headers);
4080 return Err(err);
4081 }
4082
4083 let code = http_status_to_error_code(status);
4084 let mut err = ConnectError::new(code, format!("HTTP error {}", status.as_u16()));
4085 err.set_response_headers(response_headers);
4086 return Err(err);
4087 }
4088
4089 let encoding = response
4090 .headers()
4091 .get(config.protocol.content_encoding_header())
4092 .and_then(|v| v.to_str().ok())
4093 .map(|s| s.to_owned());
4094
4095 let resp_headers = response.headers().clone();
4096
4097 let max_msg_size = options
4098 .max_message_size
4099 .unwrap_or(crate::service::DEFAULT_MAX_MESSAGE_SIZE);
4100
4101 let body_limit = max_msg_size
4106 .saturating_add(2 * crate::envelope::HEADER_SIZE)
4107 .saturating_add(RESPONSE_BUFFER_TRAILER_SLACK);
4108 let body = collect_body_bounded(response.into_body(), body_limit, deadline)
4109 .await
4110 .map_err(with_response_metadata(
4111 &resp_headers,
4112 &http::HeaderMap::new(),
4113 ))?;
4114
4115 let (data, trailers) = parse_connect_client_stream_envelopes(
4116 body,
4117 &config.compression,
4118 encoding.as_deref(),
4119 max_msg_size,
4120 &resp_headers,
4121 )?;
4122 let message = decode_response_view::<RespView>(data, config.codec_format)
4124 .map_err(with_response_metadata(&resp_headers, &trailers))?;
4125
4126 Ok(UnaryResponse {
4127 headers: resp_headers,
4128 body: message,
4129 trailers,
4130 })
4131}
4132
4133fn parse_connect_client_stream_envelopes(
4154 body: Bytes,
4155 compression: &crate::compression::CompressionRegistry,
4156 encoding: Option<&str>,
4157 max_msg_size: usize,
4158 resp_headers: &http::HeaderMap,
4159) -> Result<(Bytes, http::HeaderMap), ConnectError> {
4160 scan_connect_client_stream_envelopes(body, compression, encoding, max_msg_size).map_err(
4161 |mut err| {
4162 err.set_response_headers(resp_headers.clone());
4163 err
4164 },
4165 )
4166}
4167
4168fn scan_connect_client_stream_envelopes(
4171 body: Bytes,
4172 compression: &crate::compression::CompressionRegistry,
4173 encoding: Option<&str>,
4174 max_msg_size: usize,
4175) -> Result<(Bytes, http::HeaderMap), ConnectError> {
4176 let mut buf = BytesMut::from(body.as_ref());
4177 let mut message: Option<Bytes> = None;
4178 let mut trailers = http::HeaderMap::new();
4179 let mut saw_end_stream = false;
4180
4181 while !buf.is_empty() {
4182 let envelope = match Envelope::decode_with_limit(&mut buf, max_msg_size)? {
4183 Some(env) => env,
4184 None => break,
4185 };
4186
4187 if envelope.is_end_stream() {
4188 saw_end_stream = true;
4189 let end_stream_data = if envelope.is_compressed() {
4190 let enc = encoding.ok_or_else(|| {
4191 ConnectError::internal("received compressed END_STREAM without encoding header")
4192 })?;
4193 compression
4194 .decompress_with_limit(enc, envelope.data, max_msg_size)
4195 .map_err(map_response_decompression_error)?
4196 } else {
4197 envelope.data
4198 };
4199
4200 let end_stream = parse_connect_end_stream(&end_stream_data)?;
4201
4202 if let Some(metadata) = end_stream.metadata {
4203 append_metadata_capped(&mut trailers, metadata);
4204 }
4205
4206 if let Some(err) = end_stream.error {
4207 let mut connect_error = end_stream_error_to_connect_error(err);
4208 connect_error.set_trailers(error_metadata_from_trailers(&trailers));
4214 return Err(connect_error);
4215 }
4216
4217 if !buf.is_empty() {
4220 tracing::debug!(
4221 trailing_bytes = buf.len(),
4222 "ignoring response data after the END_STREAM envelope"
4223 );
4224 }
4225 break;
4226 }
4227
4228 if message.is_some() {
4231 return Err(ConnectError::unimplemented(
4232 "client streaming response contains multiple data messages",
4233 ));
4234 }
4235
4236 let data = if envelope.is_compressed() {
4237 let enc = encoding.ok_or_else(|| {
4238 ConnectError::internal("received compressed message without encoding header")
4239 })?;
4240 compression
4241 .decompress_with_limit(enc, envelope.data, max_msg_size)
4242 .map_err(map_response_decompression_error)?
4243 } else {
4244 envelope.data
4245 };
4246
4247 if data.len() > max_msg_size {
4248 return Err(ConnectError::new(
4249 ErrorCode::ResourceExhausted,
4250 format!("message size {} exceeds limit {}", data.len(), max_msg_size),
4251 ));
4252 }
4253
4254 message = Some(data);
4255 }
4256
4257 let message = message.ok_or_else(|| {
4258 ConnectError::unimplemented("client streaming response contains no data messages")
4259 })?;
4260
4261 if !saw_end_stream {
4265 return Err(ConnectError::internal(
4266 "Connect streaming response ended without END_STREAM envelope",
4267 ));
4268 }
4269
4270 Ok((message, trailers))
4271}
4272
4273#[derive(serde::Deserialize)]
4275struct ClientEndStreamResponse {
4276 error: Option<ClientEndStreamError>,
4277 metadata: Option<HashMap<String, Vec<String>>>,
4278}
4279
4280#[derive(serde::Deserialize)]
4282struct ClientEndStreamError {
4283 code: Option<String>,
4284 message: Option<String>,
4285 #[serde(default)]
4286 details: Vec<ErrorDetail>,
4287}
4288
4289fn parse_connect_end_stream(data: &[u8]) -> Result<ClientEndStreamResponse, ConnectError> {
4293 serde_json::from_slice(data).map_err(|e| {
4294 ConnectError::internal(format!(
4295 "protocol error: malformed Connect END_STREAM JSON: {e}"
4296 ))
4297 })
4298}
4299
4300fn end_stream_error_to_connect_error(err: ClientEndStreamError) -> ConnectError {
4303 let mut connect_error = ConnectError::new(
4304 err.code
4305 .as_deref()
4306 .and_then(|c| c.parse().ok())
4307 .unwrap_or(ErrorCode::Unknown),
4308 err.message.unwrap_or_default(),
4309 );
4310 connect_error.details = err.details;
4311 connect_error
4312}
4313
4314#[derive(serde::Deserialize)]
4316struct ConnectErrorResponse {
4317 #[serde(default)]
4318 code: Option<String>,
4319 #[serde(default)]
4320 message: Option<String>,
4321 #[serde(default)]
4322 details: Vec<ErrorDetail>,
4323}
4324
4325fn http_status_to_error_code(status: http::StatusCode) -> ErrorCode {
4330 match status.as_u16() {
4331 400 => ErrorCode::Internal,
4332 401 => ErrorCode::Unauthenticated,
4333 403 => ErrorCode::PermissionDenied,
4334 404 => ErrorCode::Unimplemented,
4335 408 => ErrorCode::DeadlineExceeded,
4336 429 => ErrorCode::Unavailable,
4337 502 => ErrorCode::Unavailable,
4338 503 => ErrorCode::Unavailable,
4339 504 => ErrorCode::Unavailable,
4340 _ => ErrorCode::Unknown,
4341 }
4342}
4343
4344fn unary_request_content_type(config: &ClientConfig) -> &'static str {
4350 match config.protocol {
4351 Protocol::Connect => config.codec_format.content_type(),
4352 Protocol::Grpc | Protocol::GrpcWeb => config
4353 .protocol
4354 .response_content_type(config.codec_format, false),
4355 }
4356}
4357
4358fn streaming_request_content_type(config: &ClientConfig) -> &'static str {
4360 config
4361 .protocol
4362 .response_content_type(config.codec_format, true)
4363}
4364
4365fn format_timeout(timeout: Duration, protocol: Protocol) -> String {
4367 encoded_timeout(timeout, protocol).header_value()
4368}
4369
4370fn add_unary_request_headers(
4378 mut builder: http::request::Builder,
4379 config: &ClientConfig,
4380 timeout: Option<Duration>,
4381 applied_content_encoding: Option<&str>,
4382) -> http::request::Builder {
4383 builder = builder.header(
4384 http::header::CONTENT_TYPE,
4385 unary_request_content_type(config),
4386 );
4387
4388 match config.protocol {
4389 Protocol::Connect => {
4390 builder = builder.header(connect_header::PROTOCOL_VERSION, "1");
4391 if let Some(encoding) = applied_content_encoding {
4394 builder = builder.header(http::header::CONTENT_ENCODING, encoding);
4395 }
4396 let accept = config.compression.accept_encoding_header();
4397 if !accept.is_empty() {
4398 builder = builder.header(http::header::ACCEPT_ENCODING, accept);
4399 }
4400 }
4401 Protocol::Grpc => {
4402 builder = builder.header("te", "trailers");
4403 if let Some(ref encoding) = config.request_compression {
4404 builder = builder.header("grpc-encoding", encoding.as_str());
4405 }
4406 let accept = config.compression.accept_encoding_header();
4407 if !accept.is_empty() {
4408 builder = builder.header("grpc-accept-encoding", accept);
4409 }
4410 }
4411 Protocol::GrpcWeb => {
4412 if let Some(ref encoding) = config.request_compression {
4413 builder = builder.header("grpc-encoding", encoding.as_str());
4414 }
4415 let accept = config.compression.accept_encoding_header();
4416 if !accept.is_empty() {
4417 builder = builder.header("grpc-accept-encoding", accept);
4418 }
4419 }
4420 }
4421
4422 if let Some(timeout) = timeout {
4423 builder = builder.header(
4424 config.protocol.timeout_header(),
4425 format_timeout(timeout, config.protocol),
4426 );
4427 }
4428
4429 builder
4430}
4431
4432fn add_streaming_request_headers(
4434 mut builder: http::request::Builder,
4435 config: &ClientConfig,
4436 timeout: Option<Duration>,
4437) -> http::request::Builder {
4438 builder = builder.header(
4439 http::header::CONTENT_TYPE,
4440 streaming_request_content_type(config),
4441 );
4442
4443 match config.protocol {
4444 Protocol::Connect => {
4445 builder = builder.header(connect_header::PROTOCOL_VERSION, "1");
4446 }
4447 Protocol::Grpc => {
4448 builder = builder.header("te", "trailers");
4449 }
4450 Protocol::GrpcWeb => {}
4451 }
4452
4453 let encoding_header = config.protocol.content_encoding_header();
4454 let accept_header = config.protocol.accept_encoding_header();
4455
4456 if let Some(ref encoding) = config.request_compression {
4457 builder = builder.header(encoding_header, encoding.as_str());
4458 }
4459 let accept = config.compression.accept_encoding_header();
4460 if !accept.is_empty() {
4461 builder = builder.header(accept_header, accept);
4462 }
4463
4464 if let Some(timeout) = timeout {
4465 builder = builder.header(
4466 config.protocol.timeout_header(),
4467 format_timeout(timeout, config.protocol),
4468 );
4469 }
4470
4471 builder
4472}
4473
4474fn is_status_trailer(name: &http::HeaderName) -> bool {
4480 *name == hdr::GRPC_STATUS || *name == hdr::GRPC_MESSAGE || *name == hdr::GRPC_STATUS_DETAILS_BIN
4481}
4482
4483fn error_metadata_from_trailers(trailers: &http::HeaderMap) -> http::HeaderMap {
4489 let mut out = http::HeaderMap::new();
4490 for (key, value) in trailers {
4491 if !is_status_trailer(key) {
4492 out.append(key, value.clone());
4493 }
4494 }
4495 out
4496}
4497
4498fn parse_grpc_error_from_trailers(trailers: &http::HeaderMap) -> Option<ConnectError> {
4500 let raw = trailers.get("grpc-status")?;
4501 let Some(status) = raw.to_str().ok().and_then(|s| s.parse::<u32>().ok()) else {
4505 return Some(ConnectError::new(
4506 ErrorCode::Unknown,
4507 format!("protocol error: malformed grpc-status: {raw:?}"),
4508 ));
4509 };
4510
4511 if status == 0 {
4512 return None; }
4514
4515 let code = ErrorCode::from_grpc_code(status).unwrap_or(ErrorCode::Unknown);
4516 let message = trailers
4517 .get("grpc-message")
4518 .and_then(|v| v.to_str().ok())
4519 .map(grpc_percent_decode);
4520
4521 let mut err = ConnectError::new(code, message.unwrap_or_default());
4522
4523 if let Some(details_b64) = trailers
4525 .get("grpc-status-details-bin")
4526 .and_then(|v| v.to_str().ok())
4527 {
4528 use base64::Engine;
4529 if let Ok(details_bytes) = base64::engine::general_purpose::STANDARD
4530 .decode(details_b64)
4531 .or_else(|_| base64::engine::general_purpose::STANDARD_NO_PAD.decode(details_b64))
4532 {
4533 err.details = crate::grpc_status::decode_details(&details_bytes);
4534 }
4535 }
4536
4537 err.set_trailers(error_metadata_from_trailers(trailers));
4538
4539 Some(err)
4540}
4541
4542async fn collect_body_bounded<B>(
4553 body: B,
4554 max_size: usize,
4555 deadline: Option<std::time::Instant>,
4556) -> Result<Bytes, ConnectError>
4557where
4558 B: Body<Data = Bytes>,
4559 B::Error: std::fmt::Display,
4560{
4561 let mut buf = BytesMut::new();
4562 let mut stream = std::pin::pin!(body);
4563 loop {
4564 match std::future::poll_fn(|cx| stream.as_mut().poll_frame(cx)).await {
4565 Some(Ok(frame)) => {
4566 if let Ok(data) = frame.into_data() {
4570 if buf.len().saturating_add(data.len()) > max_size {
4571 return Err(ConnectError::new(
4572 ErrorCode::ResourceExhausted,
4573 format!("response body size exceeds limit {max_size}"),
4574 ));
4575 }
4576 buf.extend_from_slice(&data);
4577 }
4578 }
4579 Some(Err(e)) => {
4580 return Err(classify_body_read_error(
4581 "failed to read response body",
4582 &e,
4583 deadline,
4584 ));
4585 }
4586 None => break,
4587 }
4588 }
4589 Ok(buf.freeze())
4590}
4591
4592fn grpc_percent_decode(s: &str) -> String {
4596 percent_encoding::percent_decode_str(s)
4597 .decode_utf8_lossy()
4598 .into_owned()
4599}
4600
4601fn parse_grpc_web_trailer_frame_with_compression(
4605 data: &[u8],
4606 decompression: Option<(&CompressionRegistry, &str)>,
4607) -> Option<http::HeaderMap> {
4608 if data.len() < 5 || data[0] & 0x80 == 0 {
4609 return None;
4610 }
4611 let is_compressed = data[0] & 0x01 != 0;
4612 let len = u32::from_be_bytes([data[1], data[2], data[3], data[4]]) as usize;
4613 const MAX_TRAILER_SIZE: usize = 1024 * 1024;
4616 if len > MAX_TRAILER_SIZE || data.len() < 5 + len {
4617 return None;
4618 }
4619 let raw_payload = &data[5..5 + len];
4620
4621 let payload_bytes;
4623 let payload = if is_compressed {
4624 if let Some((registry, encoding)) = decompression {
4625 payload_bytes = registry
4626 .decompress_with_limit(
4627 encoding,
4628 Bytes::copy_from_slice(raw_payload),
4629 MAX_TRAILER_SIZE,
4630 )
4631 .ok()?;
4632 std::str::from_utf8(&payload_bytes).ok()?
4633 } else {
4634 return None;
4635 }
4636 } else {
4637 std::str::from_utf8(raw_payload).ok()?
4638 };
4639
4640 let mut headers = http::HeaderMap::new();
4641 for line in payload.split('\n') {
4643 let line = line.trim_end_matches('\r');
4644 if line.is_empty() {
4645 continue;
4646 }
4647 if let Some((key, value)) = line.split_once(':')
4649 && let (Ok(name), Ok(val)) = (
4650 http::header::HeaderName::from_bytes(key.trim().as_bytes()),
4651 http::HeaderValue::from_str(value.trim()),
4652 )
4653 {
4654 if headers.try_append(name, val).is_err() {
4662 break;
4663 }
4664 }
4665 }
4666 Some(headers)
4667}
4668
4669fn append_metadata_capped(trailers: &mut http::HeaderMap, metadata: HashMap<String, Vec<String>>) {
4679 'outer: for (name, values) in metadata {
4680 for value in values {
4681 if let (Ok(name), Ok(value)) = (
4682 http::header::HeaderName::from_bytes(name.as_bytes()),
4683 http::header::HeaderValue::from_str(&value),
4684 ) && trailers.try_append(name, value).is_err()
4685 {
4686 break 'outer;
4687 }
4688 }
4689 }
4690}
4691
4692#[cfg(test)]
4693mod tests {
4694 use super::*;
4695
4696 #[test]
4697 fn overflow_payload_is_internal_at_response_decode() {
4698 use buffa_types::google::protobuf::__buffa::view::StringValueView;
4699
4700 let body = crate::request::tests::unknown_field_overflow_body();
4705 let err =
4706 decode_response_view::<StringValueView<'static>>(body, CodecFormat::Proto).unwrap_err();
4707 assert_eq!(err.code, ErrorCode::Internal);
4708 }
4709
4710 #[test]
4711 fn into_owned_parts_preserves_metadata() {
4712 use buffa::Message;
4713 use buffa::view::OwnedView;
4714 use buffa_types::google::protobuf::__buffa::view::StringValueView;
4715 use buffa_types::google::protobuf::StringValue;
4716
4717 let bytes = Bytes::from(StringValue::from("with-metadata").encode_to_vec());
4718 let view: OwnedView<StringValueView<'static>> = OwnedView::decode(bytes).unwrap();
4719 let mut headers = http::HeaderMap::new();
4720 headers.insert("x-probe", http::HeaderValue::from_static("h"));
4721 let mut trailers = http::HeaderMap::new();
4722 trailers.insert("x-trailer", http::HeaderValue::from_static("t"));
4723 let resp = UnaryResponse {
4724 headers,
4725 body: view,
4726 trailers,
4727 };
4728
4729 let (headers, owned, trailers) = resp.into_owned_parts();
4730 assert_eq!(owned.value, "with-metadata");
4731 assert_eq!(headers.get("x-probe").unwrap(), "h");
4732 assert_eq!(trailers.get("x-trailer").unwrap(), "t");
4733 }
4734
4735 #[cfg(feature = "json")]
4736 #[test]
4737 fn test_client_config() {
4738 let config = ClientConfig::new("http://localhost:8080".parse().unwrap())
4739 .json()
4740 .compress_requests("gzip");
4741
4742 assert_eq!(config.codec_format, CodecFormat::Json);
4743 assert_eq!(config.request_compression, Some("gzip".to_string()));
4744 }
4745
4746 #[cfg(not(feature = "json"))]
4747 #[test]
4748 fn test_client_config_proto_only() {
4749 let config =
4752 ClientConfig::new("http://localhost:8080".parse().unwrap()).compress_requests("gzip");
4753
4754 assert_eq!(config.codec_format, CodecFormat::Proto);
4755 assert_eq!(config.request_compression, Some("gzip".to_string()));
4756 }
4757
4758 #[cfg(feature = "client")]
4759 #[tokio::test]
4760 async fn http_client_connect_timeout_bounds_tcp_connect() {
4761 use std::time::Instant;
4762
4763 let target = "http://192.0.2.1:9/";
4769 let timeout = Duration::from_millis(100);
4770
4771 let http = HttpClient::builder().connect_timeout(timeout).plaintext();
4772 let req = http::Request::builder()
4773 .method(http::Method::POST)
4774 .uri(target)
4775 .body(full_body(Bytes::new()))
4776 .unwrap();
4777
4778 let start = Instant::now();
4779 let result = tokio::time::timeout(Duration::from_secs(3), http.send(req)).await;
4783 let elapsed = start.elapsed();
4784 let Ok(Err(err)) = result else {
4785 eprintln!("skipping: TEST-NET-1 reachable on this host (proxy?) in {elapsed:?}");
4786 return;
4787 };
4788
4789 assert!(
4792 elapsed < Duration::from_secs(2),
4793 "connect_timeout(100ms) should abort within ~2s, took {elapsed:?}: {err}"
4794 );
4795 }
4796
4797 #[cfg(feature = "client")]
4798 #[tokio::test]
4799 async fn http_client_establishment_timeout_bounds_plaintext_connector() {
4800 use std::time::Instant;
4801
4802 let target = "http://192.0.2.1:9/";
4806 let http = HttpClient::builder()
4807 .establishment_timeout(Duration::from_millis(100))
4808 .plaintext();
4809 let req = http::Request::builder()
4810 .method(http::Method::POST)
4811 .uri(target)
4812 .body(full_body(Bytes::new()))
4813 .unwrap();
4814
4815 let start = Instant::now();
4816 let result = tokio::time::timeout(Duration::from_secs(3), http.send(req)).await;
4820 let elapsed = start.elapsed();
4821 let Ok(Err(err)) = result else {
4822 eprintln!("skipping: TEST-NET-1 reachable on this host (proxy?) in {elapsed:?}");
4823 return;
4824 };
4825
4826 assert!(
4827 elapsed < Duration::from_secs(2),
4828 "establishment_timeout(100ms) should abort within ~2s, took {elapsed:?}: {err}"
4829 );
4830 }
4831
4832 #[cfg(feature = "client-tls")]
4833 #[tokio::test]
4834 async fn http_client_establishment_timeout_bounds_stalled_tls() {
4835 use std::time::Instant;
4836
4837 let listener = tokio::net::TcpListener::bind("127.0.0.1:0").await.unwrap();
4841 let addr = listener.local_addr().unwrap();
4842 let server = tokio::spawn(async move {
4843 let mut held = Vec::new();
4844 while let Ok((stream, _)) = listener.accept().await {
4845 held.push(stream);
4846 }
4847 });
4848
4849 let tls_config = std::sync::Arc::new(
4850 rustls::ClientConfig::builder()
4851 .with_root_certificates(rustls::RootCertStore::empty())
4852 .with_no_client_auth(),
4853 );
4854 let http = HttpClient::builder()
4855 .establishment_timeout(Duration::from_millis(150))
4856 .with_tls(tls_config);
4857 let req = http::Request::builder()
4858 .method(http::Method::POST)
4859 .uri(format!("https://{addr}/"))
4860 .body(full_body(Bytes::new()))
4861 .unwrap();
4862
4863 let start = Instant::now();
4864 let err = http.send(req).await.expect_err("stalled TLS must fail");
4865 let elapsed = start.elapsed();
4866
4867 assert!(
4868 elapsed < Duration::from_secs(2),
4869 "establishment_timeout(150ms) should fire within ~2s, took {elapsed:?}: {err}"
4870 );
4871
4872 server.abort();
4873 }
4874
4875 #[cfg(feature = "client")]
4876 #[tokio::test]
4877 async fn http_client_plaintext_send_failure_preserves_source() {
4878 let http = HttpClient::plaintext();
4879 let req = http::Request::builder()
4881 .method(http::Method::POST)
4882 .uri("http://127.0.0.1:1/")
4883 .body(full_body(Bytes::new()))
4884 .unwrap();
4885
4886 let err = http
4887 .send(req)
4888 .await
4889 .expect_err("connect to port 1 must fail");
4890 assert!(
4891 err.message
4892 .as_deref()
4893 .unwrap()
4894 .contains("HTTP request failed"),
4895 "unexpected message: {err:?}"
4896 );
4897 assert!(
4898 std::error::Error::source(&err).is_some(),
4899 "connection-refused failure must retain its cause as source(): {err:?}"
4900 );
4901 }
4902
4903 #[cfg(feature = "client-tls")]
4904 #[tokio::test]
4905 async fn http_client_tls_send_failure_preserves_source() {
4906 let tls_config = std::sync::Arc::new(
4907 rustls::ClientConfig::builder()
4908 .with_root_certificates(rustls::RootCertStore::empty())
4909 .with_no_client_auth(),
4910 );
4911 let http = HttpClient::with_tls(tls_config);
4912 let req = http::Request::builder()
4915 .method(http::Method::POST)
4916 .uri("https://127.0.0.1:1/")
4917 .body(full_body(Bytes::new()))
4918 .unwrap();
4919
4920 let err = http
4921 .send(req)
4922 .await
4923 .expect_err("connect to port 1 must fail");
4924 assert!(
4925 err.message
4926 .as_deref()
4927 .unwrap()
4928 .contains("HTTPS request failed"),
4929 "unexpected message: {err:?}"
4930 );
4931 assert!(
4932 std::error::Error::source(&err).is_some(),
4933 "connection-refused failure must retain its cause as source(): {err:?}"
4934 );
4935 }
4936
4937 #[test]
4938 fn client_config_builders_round_trip_through_accessors() {
4939 let mut headers = http::HeaderMap::new();
4943 headers.insert("x-base", "v".parse().unwrap());
4944
4945 let config = ClientConfig::new("http://example.com:8080".parse().unwrap())
4946 .with_protocol(Protocol::Grpc)
4947 .with_codec_format(CodecFormat::Json)
4948 .with_compression(CompressionRegistry::default())
4949 .compress_requests("gzip")
4950 .with_compression_policy(CompressionPolicy::default())
4951 .with_default_timeout(Duration::from_secs(30))
4952 .with_default_max_message_size(4096)
4953 .with_default_headers(headers.clone())
4954 .with_default_header("x-extra", "1");
4955
4956 assert_eq!(config.base_uri().to_string(), "http://example.com:8080/");
4957 assert_eq!(config.protocol(), Protocol::Grpc);
4958 assert_eq!(config.codec_format(), CodecFormat::Json);
4959 assert_eq!(config.request_compression(), Some("gzip"));
4960 assert_eq!(config.default_timeout(), Some(Duration::from_secs(30)));
4961 assert_eq!(config.default_max_message_size(), Some(4096));
4962 assert_eq!(config.default_headers().get("x-base").unwrap(), "v");
4963 assert_eq!(config.default_headers().get("x-extra").unwrap(), "1");
4964 let _ = config.compression();
4968 let _ = config.compression_policy();
4969 }
4970
4971 #[test]
4972 fn client_config_defaults() {
4973 let config = ClientConfig::new("http://localhost".parse().unwrap());
4974 assert_eq!(config.protocol(), Protocol::Connect);
4975 assert_eq!(config.codec_format(), CodecFormat::Proto);
4976 assert_eq!(config.request_compression(), None);
4977 assert_eq!(config.default_timeout(), None);
4978 assert_eq!(config.default_max_message_size(), None);
4979 assert!(config.default_headers().is_empty());
4980 }
4981
4982 #[test]
4983 fn call_options_builders_round_trip_through_accessors() {
4984 let options = CallOptions::default()
4985 .with_timeout(Duration::from_secs(5))
4986 .with_header("x-request-id", "abc")
4987 .with_max_message_size(2048)
4988 .with_compress(true);
4989
4990 assert_eq!(options.timeout(), Some(Duration::from_secs(5)));
4991 assert_eq!(options.headers().get("x-request-id").unwrap(), "abc");
4992 assert_eq!(options.max_message_size(), Some(2048));
4993 assert_eq!(options.compress(), Some(true));
4994 }
4995
4996 #[test]
4997 fn call_options_defaults() {
4998 let options = CallOptions::default();
4999 assert!(options.headers().is_empty());
5000 assert_eq!(options.timeout(), None);
5001 assert_eq!(options.max_message_size(), None);
5002 assert_eq!(options.compress(), None);
5003 }
5004
5005 #[cfg(feature = "client")]
5006 #[test]
5007 fn test_http_client_plaintext_creation() {
5008 let _client = HttpClient::plaintext();
5009 let _client = HttpClient::plaintext_http2_only();
5010 }
5011
5012 #[test]
5018 fn client_types_are_debug() {
5019 fn assert_debug<T: std::fmt::Debug>() {}
5020
5021 assert_debug::<UnaryResponse<()>>();
5025
5026 assert_debug::<ServerStream<http_body_util::Empty<Bytes>, ()>>();
5029 assert_debug::<BidiStream<http_body_util::Empty<Bytes>, (), ()>>();
5030 assert_debug::<BidiSendHalf<()>>();
5031 assert_debug::<BidiRecvHalf<http_body_util::Empty<Bytes>, ()>>();
5032
5033 #[cfg(feature = "client")]
5035 assert_debug::<HttpClient>();
5036 }
5037
5038 #[test]
5039 fn bidi_stream_auto_traits() {
5040 fn assert_send<T: Send>() {}
5041 fn assert_sync<T: Sync>() {}
5042 fn assert_unpin<T: Unpin>() {}
5043
5044 type TestBidi = BidiStream<http_body_util::Empty<Bytes>, (), ()>;
5045
5046 assert_send::<TestBidi>();
5047 assert_sync::<TestBidi>();
5048 assert_unpin::<TestBidi>();
5049
5050 type TestSend = BidiSendHalf<()>;
5053 type TestRecv = BidiRecvHalf<http_body_util::Empty<Bytes>, ()>;
5054
5055 assert_send::<TestSend>();
5056 assert_sync::<TestSend>();
5057 assert_unpin::<TestSend>();
5058 assert_send::<TestRecv>();
5059 assert_sync::<TestRecv>();
5060 assert_unpin::<TestRecv>();
5061 }
5062
5063 fn connect_success_body(message: &str) -> Bytes {
5064 use buffa::Message;
5065 use buffa_types::google::protobuf::StringValue;
5066
5067 let mut body = BytesMut::new();
5068 body.extend_from_slice(
5069 &Envelope::data(StringValue::from(message).encode_to_bytes()).encode(),
5070 );
5071 body.extend_from_slice(&Envelope::end_stream(Bytes::from_static(b"{}")).encode());
5072 body.freeze()
5073 }
5074
5075 fn connect_response<B>(status: http::StatusCode, body: B) -> Response<B> {
5076 Response::builder().status(status).body(body).unwrap()
5077 }
5078
5079 fn bidi_stream_with_response_task<B>(
5080 response_task: tokio::task::JoinHandle<Result<Response<B>, ConnectError>>,
5081 deadline: Option<std::time::Instant>,
5082 ) -> BidiStream<
5083 B,
5084 buffa_types::google::protobuf::StringValue,
5085 buffa_types::google::protobuf::__buffa::view::StringValueView<'static>,
5086 > {
5087 BidiStream {
5088 send: BidiSendHalf {
5089 tx: None,
5090 encoder: crate::envelope::EnvelopeEncoder::uncompressed(),
5091 codec_format: CodecFormat::Proto,
5092 deadline,
5093 _req: PhantomData,
5094 },
5095 recv: BidiRecvHalf {
5096 recv: RecvState::AwaitingHeaders(response_task),
5097 stream_config: StreamConfig {
5098 protocol: Protocol::Connect,
5099 codec_format: CodecFormat::Proto,
5100 compression: CompressionRegistry::new(),
5101 max_message_size: Some(1024),
5102 deadline,
5103 },
5104 },
5105 }
5106 }
5107
5108 struct GatedBody {
5109 shared: std::sync::Arc<std::sync::Mutex<GatedBodyState>>,
5110 }
5111
5112 struct GatedBodyRelease {
5113 shared: std::sync::Arc<std::sync::Mutex<GatedBodyState>>,
5114 }
5115
5116 struct GatedBodyState {
5117 first_poll: Option<tokio::sync::oneshot::Sender<()>>,
5118 released: Option<Bytes>,
5119 done: bool,
5120 waker: Option<std::task::Waker>,
5121 }
5122
5123 impl GatedBody {
5124 fn new() -> (Self, tokio::sync::oneshot::Receiver<()>, GatedBodyRelease) {
5125 let (first_poll_tx, first_poll_rx) = tokio::sync::oneshot::channel();
5126 let shared = std::sync::Arc::new(std::sync::Mutex::new(GatedBodyState {
5127 first_poll: Some(first_poll_tx),
5128 released: None,
5129 done: false,
5130 waker: None,
5131 }));
5132
5133 (
5134 Self {
5135 shared: shared.clone(),
5136 },
5137 first_poll_rx,
5138 GatedBodyRelease { shared },
5139 )
5140 }
5141 }
5142
5143 impl GatedBodyRelease {
5144 fn release(self, bytes: Bytes) {
5145 let mut state = self.shared.lock().unwrap();
5146 state.released = Some(bytes);
5147 if let Some(waker) = state.waker.take() {
5148 waker.wake();
5149 }
5150 }
5151 }
5152
5153 impl Body for GatedBody {
5154 type Data = Bytes;
5155 type Error = ConnectError;
5156
5157 fn poll_frame(
5158 self: Pin<&mut Self>,
5159 cx: &mut std::task::Context<'_>,
5160 ) -> std::task::Poll<Option<Result<http_body::Frame<Bytes>, ConnectError>>> {
5161 let mut state = self.shared.lock().unwrap();
5162 if let Some(first_poll) = state.first_poll.take() {
5163 let _ = first_poll.send(());
5164 }
5165
5166 if state.done {
5167 return std::task::Poll::Ready(None);
5168 }
5169
5170 if let Some(bytes) = state.released.take() {
5171 state.done = true;
5172 return std::task::Poll::Ready(Some(Ok(http_body::Frame::data(bytes))));
5173 }
5174
5175 state.waker = Some(cx.waker().clone());
5176 std::task::Poll::Pending
5177 }
5178 }
5179
5180 #[tokio::test]
5181 async fn bidi_message_cancel_before_headers_resumes() {
5182 use buffa_types::google::protobuf::StringValue;
5183
5184 let (response_tx, response_rx) =
5185 tokio::sync::oneshot::channel::<Result<Response<Full<Bytes>>, ConnectError>>();
5186 let response_task = tokio::spawn(async move {
5187 response_rx
5188 .await
5189 .expect("test response sender should stay alive")
5190 });
5191 let mut stream = bidi_stream_with_response_task(response_task, None);
5192
5193 let mut first_message = Box::pin(stream.message::<StringValue>());
5194 assert!(matches!(
5195 futures::poll!(&mut first_message),
5196 std::task::Poll::Pending
5197 ));
5198 drop(first_message);
5199
5200 assert!(stream.error().is_none());
5201 assert!(stream.headers().is_none());
5202
5203 response_tx
5204 .send(Ok(connect_response(
5205 http::StatusCode::OK,
5206 Full::new(connect_success_body("hello")),
5207 )))
5208 .expect("detached response task should still receive headers");
5209
5210 let msg = stream
5211 .message::<StringValue>()
5212 .await
5213 .expect("cancelled header wait should resume")
5214 .expect("stream should yield first response message");
5215 assert_eq!(msg.view().value, "hello");
5216 assert!(stream.message::<StringValue>().await.unwrap().is_none());
5217 }
5218
5219 #[tokio::test]
5220 async fn bidi_message_cancel_during_connect_error_body_resumes() {
5221 use buffa_types::google::protobuf::StringValue;
5222
5223 let (body, body_polled, body_release) = GatedBody::new();
5224 let (response_ready_tx, response_ready_rx) = tokio::sync::oneshot::channel();
5225 let response_task = tokio::spawn(async move {
5226 let response = connect_response(http::StatusCode::BAD_REQUEST, body);
5227 let _ = response_ready_tx.send(());
5228 Ok(response)
5229 });
5230 response_ready_rx
5231 .await
5232 .expect("response task should have prepared headers");
5233 let mut stream = bidi_stream_with_response_task(response_task, None);
5234
5235 let mut first_message = Box::pin(stream.message::<StringValue>());
5236 assert!(matches!(
5237 futures::poll!(&mut first_message),
5238 std::task::Poll::Pending
5239 ));
5240 body_polled
5241 .await
5242 .expect("Connect error body should be polled");
5243 drop(first_message);
5244
5245 assert!(stream.error().is_none());
5246 assert!(stream.headers().is_none());
5247
5248 body_release.release(Bytes::from_static(
5249 br#"{"code":"invalid_argument","message":"bad request"}"#,
5250 ));
5251
5252 let err = stream
5253 .message::<StringValue>()
5254 .await
5255 .expect_err("server Connect error should be preserved");
5256 assert_eq!(err.code, ErrorCode::InvalidArgument);
5257 assert_eq!(err.message.as_deref(), Some("bad request"));
5258 let again = stream
5259 .message::<StringValue>()
5260 .await
5261 .expect_err("initialization error should be sticky");
5262 assert_eq!(again.code, ErrorCode::InvalidArgument);
5263 assert_eq!(stream.error().unwrap().code, ErrorCode::InvalidArgument);
5264 }
5265
5266 #[tokio::test(start_paused = true)]
5267 async fn bidi_message_deadline_before_headers_stays_sticky() {
5268 use buffa_types::google::protobuf::StringValue;
5269
5270 let (_response_tx, response_rx) =
5271 tokio::sync::oneshot::channel::<Result<Response<Full<Bytes>>, ConnectError>>();
5272 let response_task = tokio::spawn(async move {
5273 response_rx
5274 .await
5275 .expect("test intentionally keeps response pending")
5276 });
5277 let deadline = std::time::Instant::now() + Duration::from_millis(100);
5278 let mut stream = bidi_stream_with_response_task(response_task, Some(deadline));
5279
5280 let err = stream
5281 .message::<StringValue>()
5282 .await
5283 .expect_err("deadline should fail receive initialization");
5284 assert_eq!(err.code, ErrorCode::DeadlineExceeded);
5285
5286 let again = stream
5287 .message::<StringValue>()
5288 .await
5289 .expect_err("deadline failure should be sticky");
5290 assert_eq!(again.code, ErrorCode::DeadlineExceeded);
5291 assert_eq!(stream.error().unwrap().code, ErrorCode::DeadlineExceeded);
5292 }
5293
5294 #[tokio::test(start_paused = true)]
5295 async fn bidi_message_deadline_during_connect_error_body_stays_sticky() {
5296 use buffa_types::google::protobuf::StringValue;
5297
5298 let (body, body_polled, _body_release) = GatedBody::new();
5299 let (response_ready_tx, response_ready_rx) = tokio::sync::oneshot::channel();
5300 let response_task = tokio::spawn(async move {
5301 let response = connect_response(http::StatusCode::BAD_REQUEST, body);
5302 let _ = response_ready_tx.send(());
5303 Ok(response)
5304 });
5305 response_ready_rx
5306 .await
5307 .expect("response task should have prepared headers");
5308
5309 let deadline = std::time::Instant::now() + Duration::from_millis(100);
5310 let mut stream = bidi_stream_with_response_task(response_task, Some(deadline));
5311
5312 let mut first_message = Box::pin(stream.message::<StringValue>());
5313 assert!(matches!(
5314 futures::poll!(&mut first_message),
5315 std::task::Poll::Pending
5316 ));
5317 body_polled
5318 .await
5319 .expect("Connect error body should be polled");
5320
5321 let err = first_message
5324 .await
5325 .expect_err("deadline should fail response construction");
5326 assert_eq!(err.code, ErrorCode::DeadlineExceeded);
5327
5328 let again = stream
5329 .message::<StringValue>()
5330 .await
5331 .expect_err("construction deadline should be sticky");
5332 assert_eq!(again.code, ErrorCode::DeadlineExceeded);
5333 assert_eq!(stream.error().unwrap().code, ErrorCode::DeadlineExceeded);
5334 }
5335
5336 #[tokio::test]
5337 async fn bidi_message_transport_failure_is_sticky() {
5338 use buffa_types::google::protobuf::StringValue;
5339
5340 let response_task = tokio::spawn(async {
5341 Err::<Response<Full<Bytes>>, _>(ConnectError::unavailable("request failed: boom"))
5342 });
5343 let mut stream = bidi_stream_with_response_task(response_task, None);
5344
5345 let err = stream
5346 .message::<StringValue>()
5347 .await
5348 .expect_err("transport failure should fail receive initialization");
5349 assert_eq!(err.code, ErrorCode::Unavailable);
5350 assert_eq!(err.message.as_deref(), Some("request failed: boom"));
5351
5352 let again = stream
5353 .message::<StringValue>()
5354 .await
5355 .expect_err("transport failure should be sticky");
5356 assert_eq!(again.code, ErrorCode::Unavailable);
5357 assert_eq!(again.message.as_deref(), Some("request failed: boom"));
5358 assert_eq!(stream.error().unwrap().code, ErrorCode::Unavailable);
5359 }
5360
5361 #[tokio::test]
5362 async fn bidi_drop_aborts_headers_task() {
5363 let (guard_tx, guard_rx) = tokio::sync::oneshot::channel::<()>();
5364 let (_never_tx, never_rx) =
5365 tokio::sync::oneshot::channel::<Result<Response<Full<Bytes>>, ConnectError>>();
5366 let response_task = tokio::spawn(async move {
5367 let _guard = guard_tx;
5368 never_rx.await.expect("test never resolves the response")
5369 });
5370 let stream = bidi_stream_with_response_task(response_task, None);
5371 drop(stream);
5372
5373 tokio::time::timeout(Duration::from_secs(5), guard_rx)
5376 .await
5377 .expect("dropped BidiStream should abort the headers task")
5378 .expect_err("guard sender should be dropped by the abort");
5379 }
5380
5381 #[tokio::test]
5382 async fn bidi_drop_aborts_pending_construction() {
5383 use buffa_types::google::protobuf::StringValue;
5384
5385 let (body, body_polled, body_release) = GatedBody::new();
5386 let (response_ready_tx, response_ready_rx) = tokio::sync::oneshot::channel();
5387 let response_task = tokio::spawn(async move {
5388 let response = connect_response(http::StatusCode::BAD_REQUEST, body);
5389 let _ = response_ready_tx.send(());
5390 Ok(response)
5391 });
5392 response_ready_rx
5393 .await
5394 .expect("response task should have prepared headers");
5395 let mut stream = bidi_stream_with_response_task(response_task, None);
5396
5397 let mut first_message = Box::pin(stream.message::<StringValue>());
5398 assert!(matches!(
5399 futures::poll!(&mut first_message),
5400 std::task::Poll::Pending
5401 ));
5402 body_polled
5403 .await
5404 .expect("Connect error body should be polled");
5405 drop(first_message);
5406 drop(stream);
5407
5408 tokio::time::timeout(Duration::from_secs(5), async {
5411 while std::sync::Arc::strong_count(&body_release.shared) > 1 {
5412 tokio::task::yield_now().await;
5413 }
5414 })
5415 .await
5416 .expect("dropped BidiStream should abort construction and drop the body");
5417 }
5418
5419 #[tokio::test(start_paused = true)]
5420 async fn bidi_deadline_aborts_headers_task() {
5421 use buffa_types::google::protobuf::StringValue;
5422
5423 let (guard_tx, guard_rx) = tokio::sync::oneshot::channel::<()>();
5424 let (_never_tx, never_rx) =
5425 tokio::sync::oneshot::channel::<Result<Response<Full<Bytes>>, ConnectError>>();
5426 let response_task = tokio::spawn(async move {
5427 let _guard = guard_tx;
5428 never_rx.await.expect("test never resolves the response")
5429 });
5430 let deadline = std::time::Instant::now() + Duration::from_millis(100);
5431 let mut stream = bidi_stream_with_response_task(response_task, Some(deadline));
5432
5433 let err = stream
5434 .message::<StringValue>()
5435 .await
5436 .expect_err("deadline should fail receive initialization");
5437 assert_eq!(err.code, ErrorCode::DeadlineExceeded);
5438
5439 tokio::time::timeout(Duration::from_secs(5), guard_rx)
5442 .await
5443 .expect("deadline failure should abort the headers task")
5444 .expect_err("guard sender should be dropped by the abort");
5445 }
5446
5447 #[tokio::test]
5448 async fn connect_server_stream_truncated_after_data_errors() {
5449 use buffa::Message;
5450 use buffa_types::google::protobuf::__buffa::view::StringValueView;
5451 use buffa_types::google::protobuf::StringValue;
5452
5453 let body = Full::new(Envelope::data(StringValue::from("hello").encode_to_bytes()).encode());
5454 let mut stream: ServerStream<_, StringValueView<'static>> = ServerStream {
5455 headers: http::HeaderMap::new(),
5456 body,
5457 buf: BytesMut::new(),
5458 encoding: None,
5459 compression: CompressionRegistry::new(),
5460 codec_format: CodecFormat::Proto,
5461 protocol: Protocol::Connect,
5462 max_message_size: Some(1024),
5463 deadline: None,
5464 end: None,
5465 saw_body_data: false,
5466 _phantom: PhantomData,
5467 };
5468
5469 let msg = stream
5470 .message()
5471 .await
5472 .expect("first message should decode")
5473 .expect("stream should yield the data envelope before EOF");
5474 assert_eq!(msg.view().value, "hello");
5475
5476 let err = match stream.message().await {
5477 Err(err) => err,
5478 Ok(Some(_)) => panic!("truncated stream unexpectedly yielded another message"),
5479 Ok(None) => panic!("truncated stream ended cleanly without END_STREAM"),
5480 };
5481 assert_eq!(err.code, ErrorCode::Internal);
5482 assert!(
5483 err.to_string().contains("END_STREAM"),
5484 "unexpected error: {err}"
5485 );
5486
5487 let again = stream
5490 .message()
5491 .await
5492 .expect_err("truncation error must be sticky");
5493 assert_eq!(again.code, ErrorCode::Internal);
5494 }
5495
5496 #[tokio::test]
5500 async fn connect_server_stream_empty_body_errors() {
5501 use buffa_types::google::protobuf::__buffa::view::StringValueView;
5502
5503 let body = Full::new(Bytes::new());
5504 let mut stream: ServerStream<_, StringValueView<'static>> = ServerStream {
5505 headers: http::HeaderMap::new(),
5506 body,
5507 buf: BytesMut::new(),
5508 encoding: None,
5509 compression: CompressionRegistry::new(),
5510 codec_format: CodecFormat::Proto,
5511 protocol: Protocol::Connect,
5512 max_message_size: Some(1024),
5513 deadline: None,
5514 end: None,
5515 saw_body_data: false,
5516 _phantom: PhantomData,
5517 };
5518
5519 let err = match stream.message().await {
5520 Err(err) => err,
5521 Ok(Some(_)) => panic!("empty body unexpectedly yielded a message"),
5522 Ok(None) => panic!("empty body without END_STREAM ended cleanly"),
5523 };
5524 assert_eq!(err.code, ErrorCode::Internal);
5525 assert!(
5526 err.to_string().contains("END_STREAM"),
5527 "unexpected error: {err}"
5528 );
5529
5530 let again = stream
5531 .message()
5532 .await
5533 .expect_err("truncation error must be sticky");
5534 assert_eq!(again.code, ErrorCode::Internal);
5535 }
5536
5537 #[tokio::test]
5541 async fn connect_end_stream_error_returned_from_message() {
5542 use buffa::Message;
5543 use buffa_types::google::protobuf::__buffa::view::StringValueView;
5544 use buffa_types::google::protobuf::StringValue;
5545
5546 let mut body = BytesMut::new();
5547 body.extend_from_slice(
5548 &Envelope::data(StringValue::from("hello").encode_to_bytes()).encode(),
5549 );
5550 body.extend_from_slice(
5551 &Envelope::end_stream(Bytes::from_static(
5552 b"{\"error\":{\"code\":\"out_of_range\",\"message\":\"requested position no longer retained\"},\
5553 \"metadata\":{\"x-detail\":[\"42\"]}}",
5554 ))
5555 .encode(),
5556 );
5557 let mut stream: ServerStream<_, StringValueView<'static>> = ServerStream {
5558 headers: http::HeaderMap::new(),
5559 body: Full::new(body.freeze()),
5560 buf: BytesMut::new(),
5561 encoding: None,
5562 compression: CompressionRegistry::new(),
5563 codec_format: CodecFormat::Proto,
5564 protocol: Protocol::Connect,
5565 max_message_size: Some(1024),
5566 deadline: None,
5567 end: None,
5568 saw_body_data: false,
5569 _phantom: PhantomData,
5570 };
5571
5572 let msg = stream
5573 .message()
5574 .await
5575 .expect("data envelope should decode")
5576 .expect("stream should yield the data message first");
5577 assert_eq!(msg.view().value, "hello");
5578
5579 let err = stream
5580 .message()
5581 .await
5582 .expect_err("errored END_STREAM must surface as Err, not Ok(None)");
5583 assert_eq!(err.code, ErrorCode::OutOfRange);
5584 assert_eq!(
5585 err.message.as_deref(),
5586 Some("requested position no longer retained")
5587 );
5588
5589 let again = stream
5591 .message()
5592 .await
5593 .expect_err("terminal error is sticky");
5594 assert_eq!(again.code, ErrorCode::OutOfRange);
5595
5596 assert_eq!(stream.error().map(|e| e.code), Some(ErrorCode::OutOfRange));
5598 assert_eq!(
5599 stream
5600 .trailers()
5601 .and_then(|t| t.get("x-detail"))
5602 .and_then(|v| v.to_str().ok()),
5603 Some("42")
5604 );
5605 }
5606
5607 #[tokio::test]
5610 async fn connect_malformed_end_stream_json_errors() {
5611 use buffa::Message;
5612 use buffa_types::google::protobuf::__buffa::view::StringValueView;
5613 use buffa_types::google::protobuf::StringValue;
5614
5615 let mut body = BytesMut::new();
5616 body.extend_from_slice(
5617 &Envelope::data(StringValue::from("hello").encode_to_bytes()).encode(),
5618 );
5619 body.extend_from_slice(&Envelope::end_stream(Bytes::from_static(b"not json")).encode());
5620
5621 let mut headers = http::HeaderMap::new();
5622 headers.insert("x-from-headers", http::HeaderValue::from_static("yes"));
5623
5624 let mut stream: ServerStream<_, StringValueView<'static>> = ServerStream {
5625 headers,
5626 body: Full::new(body.freeze()),
5627 buf: BytesMut::new(),
5628 encoding: None,
5629 compression: CompressionRegistry::new(),
5630 codec_format: CodecFormat::Proto,
5631 protocol: Protocol::Connect,
5632 max_message_size: Some(1024),
5633 deadline: None,
5634 end: None,
5635 saw_body_data: false,
5636 _phantom: PhantomData,
5637 };
5638
5639 let msg = stream
5640 .message()
5641 .await
5642 .expect("data envelope should decode")
5643 .expect("stream should yield the data message first");
5644 assert_eq!(msg.view().value, "hello");
5645
5646 let err = stream
5647 .message()
5648 .await
5649 .expect_err("malformed END_STREAM must surface as Err, not Ok(None)");
5650 assert_eq!(err.code, ErrorCode::Internal);
5651 assert!(
5652 err.to_string()
5653 .contains("malformed Connect END_STREAM JSON"),
5654 "unexpected error: {err}"
5655 );
5656 assert_eq!(err.response_headers().get("x-from-headers").unwrap(), "yes");
5657
5658 let again = stream
5659 .message()
5660 .await
5661 .expect_err("malformed END_STREAM error must be sticky");
5662 assert_eq!(again.code, ErrorCode::Internal);
5663 assert_eq!(
5664 again.response_headers().get("x-from-headers").unwrap(),
5665 "yes"
5666 );
5667 }
5668
5669 #[tokio::test]
5674 async fn connect_end_stream_error_carries_response_metadata() {
5675 use buffa_types::google::protobuf::__buffa::view::StringValueView;
5676
5677 let mut body = BytesMut::new();
5678 body.extend_from_slice(
5679 &Envelope::end_stream(Bytes::from_static(
5680 b"{\"error\":{\"code\":\"permission_denied\",\"message\":\"nope\"},\
5681 \"metadata\":{\"x-from-trailers\":[\"also\"]}}",
5682 ))
5683 .encode(),
5684 );
5685
5686 let mut headers = http::HeaderMap::new();
5687 headers.insert("x-from-headers", http::HeaderValue::from_static("yes"));
5688
5689 let mut stream: ServerStream<_, StringValueView<'static>> = ServerStream {
5690 headers,
5691 body: Full::new(body.freeze()),
5692 buf: BytesMut::new(),
5693 encoding: None,
5694 compression: CompressionRegistry::new(),
5695 codec_format: CodecFormat::Proto,
5696 protocol: Protocol::Connect,
5697 max_message_size: Some(1024),
5698 deadline: None,
5699 end: None,
5700 saw_body_data: false,
5701 _phantom: PhantomData,
5702 };
5703
5704 let err = stream
5705 .message()
5706 .await
5707 .expect_err("errored END_STREAM must surface as Err");
5708 assert_eq!(err.code, ErrorCode::PermissionDenied);
5709 assert_eq!(err.response_headers().get("x-from-headers").unwrap(), "yes");
5710 assert_eq!(err.trailers().get("x-from-trailers").unwrap(), "also");
5711
5712 let again = stream
5713 .message()
5714 .await
5715 .expect_err("terminal error is sticky");
5716 assert_eq!(
5717 again.response_headers().get("x-from-headers").unwrap(),
5718 "yes"
5719 );
5720 assert_eq!(again.trailers().get("x-from-trailers").unwrap(), "also");
5721
5722 let stored = stream.error().expect("terminal error stays inspectable");
5725 assert_eq!(
5726 stored.response_headers().get("x-from-headers").unwrap(),
5727 "yes"
5728 );
5729 assert_eq!(stored.trailers().get("x-from-trailers").unwrap(), "also");
5730 }
5731
5732 #[tokio::test]
5741 async fn grpc_stream_error_carries_response_metadata() {
5742 use buffa_types::google::protobuf::__buffa::view::StringValueView;
5743 use http_body::Frame;
5744 use http_body_util::StreamBody;
5745
5746 let mut trailers = http::HeaderMap::new();
5747 trailers.insert("grpc-status", "7".parse().unwrap()); trailers.insert("grpc-message", "nope".parse().unwrap());
5749 trailers.insert("x-from-trailers", "also".parse().unwrap());
5750 let frames: Vec<Result<Frame<Bytes>, std::convert::Infallible>> =
5751 vec![Ok(Frame::trailers(trailers))];
5752
5753 let mut headers = http::HeaderMap::new();
5754 headers.insert("x-from-headers", http::HeaderValue::from_static("yes"));
5755
5756 let mut stream: ServerStream<_, StringValueView<'static>> = ServerStream {
5757 headers,
5758 body: StreamBody::new(futures::stream::iter(frames)),
5759 buf: BytesMut::new(),
5760 encoding: None,
5761 compression: CompressionRegistry::new(),
5762 codec_format: CodecFormat::Proto,
5763 protocol: Protocol::Grpc,
5764 max_message_size: Some(1024),
5765 deadline: None,
5766 end: None,
5767 saw_body_data: false,
5768 _phantom: PhantomData,
5769 };
5770
5771 let err = stream
5772 .message()
5773 .await
5774 .expect_err("grpc-status 7 must surface as Err");
5775 assert_eq!(err.code, ErrorCode::PermissionDenied);
5776 assert_eq!(err.message.as_deref(), Some("nope"));
5777 assert_eq!(err.response_headers().get("x-from-headers").unwrap(), "yes");
5778 assert_eq!(err.trailers().get("x-from-trailers").unwrap(), "also");
5779
5780 for key in [
5783 &hdr::GRPC_STATUS,
5784 &hdr::GRPC_MESSAGE,
5785 &hdr::GRPC_STATUS_DETAILS_BIN,
5786 ] {
5787 assert!(
5788 !err.trailers().contains_key(key),
5789 "{key} must not be duplicated into the error metadata"
5790 );
5791 }
5792 let wire = stream.trailers().expect("wire trailers stay available");
5793 assert_eq!(wire.get("grpc-status").unwrap(), "7");
5794 assert_eq!(wire.get("x-from-trailers").unwrap(), "also");
5795 }
5796
5797 #[tokio::test]
5802 async fn grpc_stream_error_metadata_is_empty_when_only_status_arrives() {
5803 use buffa_types::google::protobuf::__buffa::view::StringValueView;
5804 use http_body::Frame;
5805 use http_body_util::StreamBody;
5806
5807 let mut trailers = http::HeaderMap::new();
5808 trailers.insert("grpc-status", "7".parse().unwrap());
5809 let frames: Vec<Result<Frame<Bytes>, std::convert::Infallible>> =
5810 vec![Ok(Frame::trailers(trailers))];
5811
5812 let mut headers = http::HeaderMap::new();
5813 headers.insert("x-from-headers", http::HeaderValue::from_static("yes"));
5814
5815 let mut stream: ServerStream<_, StringValueView<'static>> = ServerStream {
5816 headers,
5817 body: StreamBody::new(futures::stream::iter(frames)),
5818 buf: BytesMut::new(),
5819 encoding: None,
5820 compression: CompressionRegistry::new(),
5821 codec_format: CodecFormat::Proto,
5822 protocol: Protocol::Grpc,
5823 max_message_size: Some(1024),
5824 deadline: None,
5825 end: None,
5826 saw_body_data: false,
5827 _phantom: PhantomData,
5828 };
5829
5830 let err = stream
5831 .message()
5832 .await
5833 .expect_err("grpc-status 7 must surface as Err");
5834 assert!(
5835 err.trailers().is_empty(),
5836 "status-only trailers carry no user metadata: {:?}",
5837 err.trailers()
5838 );
5839 assert_eq!(err.response_headers().get("x-from-headers").unwrap(), "yes");
5843 assert!(
5844 stream
5845 .trailers()
5846 .is_some_and(|t| t.contains_key("grpc-status")),
5847 "the wire trailers are still reported verbatim"
5848 );
5849 }
5850
5851 #[tokio::test]
5856 async fn connect_unary_content_type_rejection_carries_response_metadata() {
5857 use buffa_types::google::protobuf::__buffa::view::StringValueView;
5858
5859 let response = Response::builder()
5860 .status(http::StatusCode::OK)
5861 .header(http::header::CONTENT_TYPE, "text/html")
5862 .header("x-from-headers", "yes")
5863 .header("trailer-x-from-trailers", "also")
5864 .body(Full::new(Bytes::new()))
5865 .unwrap();
5866
5867 let config = ClientConfig::new("http://localhost".parse().unwrap());
5868 let err = parse_connect_unary_response::<_, StringValueView<'static>>(
5869 response,
5870 &config,
5871 &CallOptions::default(),
5872 None,
5873 )
5874 .await
5875 .expect_err("text/html is not a Connect response");
5876
5877 assert_eq!(err.code, ErrorCode::Unknown);
5878 assert_eq!(err.response_headers().get("x-from-headers").unwrap(), "yes");
5879 assert_eq!(err.trailers().get("x-from-trailers").unwrap(), "also");
5880 }
5881
5882 fn erroring_body_response(status: http::StatusCode) -> Response<ChannelBody> {
5885 let (tx, rx) = tokio::sync::mpsc::channel(1);
5886 tx.try_send(Err(ConnectError::internal("transport reset")))
5887 .unwrap();
5888 Response::builder()
5889 .status(status)
5890 .header(http::header::CONTENT_TYPE, "application/json")
5891 .header("x-from-headers", "yes")
5892 .body(ChannelBody { rx })
5893 .unwrap()
5894 }
5895
5896 #[tokio::test]
5902 async fn server_stream_error_body_read_failure_carries_headers() {
5903 use buffa_types::google::protobuf::__buffa::view::StringValueView;
5904
5905 let Err(err) = make_server_stream::<_, StringValueView<'static>>(
5907 erroring_body_response(http::StatusCode::INTERNAL_SERVER_ERROR),
5908 Protocol::Connect,
5909 &CompressionRegistry::default(),
5910 CodecFormat::Json,
5911 None,
5912 None,
5913 )
5914 .await
5915 else {
5916 panic!("a body that fails to read is an error");
5917 };
5918
5919 assert_eq!(
5920 err.response_headers()
5921 .get("x-from-headers")
5922 .map(|v| v.to_str().unwrap()),
5923 Some("yes"),
5924 "server-stream error-body read failure dropped the response headers"
5925 );
5926 }
5927
5928 #[tokio::test]
5930 async fn client_stream_error_body_read_failure_carries_headers() {
5931 use buffa_types::google::protobuf::__buffa::view::StringValueView;
5932
5933 let config = ClientConfig::new("http://localhost".parse().unwrap());
5934 let err = parse_connect_client_stream_response::<_, StringValueView<'static>>(
5935 erroring_body_response(http::StatusCode::INTERNAL_SERVER_ERROR),
5936 &config,
5937 &CallOptions::default(),
5938 None,
5939 )
5940 .await
5941 .expect_err("a body that fails to read is an error");
5942
5943 assert_eq!(
5944 err.response_headers()
5945 .get("x-from-headers")
5946 .map(|v| v.to_str().unwrap()),
5947 Some("yes"),
5948 "client-stream error-body read failure dropped the response headers"
5949 );
5950 }
5951
5952 #[tokio::test]
5956 async fn grpc_trailer_error_returned_from_message() {
5957 use buffa::Message;
5958 use buffa_types::google::protobuf::__buffa::view::StringValueView;
5959 use buffa_types::google::protobuf::StringValue;
5960 use http_body::Frame;
5961 use http_body_util::StreamBody;
5962
5963 let data = Envelope::data(StringValue::from("hello").encode_to_bytes()).encode();
5964 let mut trailers = http::HeaderMap::new();
5965 trailers.insert("grpc-status", "11".parse().unwrap()); trailers.insert(
5967 "grpc-message",
5968 "requested position no longer retained".parse().unwrap(),
5969 );
5970 let frames: Vec<Result<Frame<Bytes>, std::convert::Infallible>> =
5971 vec![Ok(Frame::data(data)), Ok(Frame::trailers(trailers))];
5972 let body = StreamBody::new(futures::stream::iter(frames));
5973
5974 let mut stream: ServerStream<_, StringValueView<'static>> = ServerStream {
5975 headers: http::HeaderMap::new(),
5976 body,
5977 buf: BytesMut::new(),
5978 encoding: None,
5979 compression: CompressionRegistry::new(),
5980 codec_format: CodecFormat::Proto,
5981 protocol: Protocol::Grpc,
5982 max_message_size: Some(1024),
5983 deadline: None,
5984 end: None,
5985 saw_body_data: false,
5986 _phantom: PhantomData,
5987 };
5988
5989 let msg = stream
5990 .message()
5991 .await
5992 .expect("data envelope should decode")
5993 .expect("stream should yield the data message first");
5994 assert_eq!(msg.view().value, "hello");
5995
5996 let err = stream
5997 .message()
5998 .await
5999 .expect_err("gRPC trailer error must surface as Err, not Ok(None)");
6000 assert_eq!(err.code, ErrorCode::OutOfRange);
6001 assert_eq!(
6002 err.message.as_deref(),
6003 Some("requested position no longer retained")
6004 );
6005
6006 let again = stream
6007 .message()
6008 .await
6009 .expect_err("terminal error is sticky");
6010 assert_eq!(again.code, ErrorCode::OutOfRange);
6011 assert!(stream.trailers().is_some());
6012 }
6013
6014 #[tokio::test]
6018 async fn grpc_ok_trailers_end_as_ok_none() {
6019 use std::time::Duration;
6020
6021 use buffa_types::google::protobuf::__buffa::view::StringValueView;
6022 use http_body::Frame;
6023 use http_body_util::StreamBody;
6024
6025 let mut trailers = http::HeaderMap::new();
6026 trailers.insert("grpc-status", "0".parse().unwrap());
6027 let frames: Vec<Result<Frame<Bytes>, std::convert::Infallible>> =
6028 vec![Ok(Frame::trailers(trailers))];
6029 let body = StreamBody::new(futures::stream::iter(frames));
6030
6031 let mut stream: ServerStream<_, StringValueView<'static>> = ServerStream {
6032 headers: http::HeaderMap::new(),
6033 body,
6034 buf: BytesMut::new(),
6035 encoding: None,
6036 compression: CompressionRegistry::new(),
6037 codec_format: CodecFormat::Proto,
6038 protocol: Protocol::Grpc,
6039 max_message_size: Some(1024),
6040 deadline: Some(std::time::Instant::now() + Duration::from_secs(5)),
6041 end: None,
6042 saw_body_data: false,
6043 _phantom: PhantomData,
6044 };
6045
6046 assert!(stream.message().await.unwrap().is_none());
6047 assert!(stream.error().is_none());
6048 assert!(stream.trailers().is_some());
6049 }
6050
6051 #[tokio::test]
6055 async fn grpc_eof_without_trailers_errors() {
6056 use buffa_types::google::protobuf::__buffa::view::StringValueView;
6057
6058 let mut stream: ServerStream<_, StringValueView<'static>> = ServerStream {
6059 headers: http::HeaderMap::new(),
6060 body: Full::new(Bytes::new()),
6061 buf: BytesMut::new(),
6062 encoding: None,
6063 compression: CompressionRegistry::new(),
6064 codec_format: CodecFormat::Proto,
6065 protocol: Protocol::Grpc,
6066 max_message_size: Some(1024),
6067 deadline: None,
6068 end: None,
6069 saw_body_data: false,
6070 _phantom: PhantomData,
6071 };
6072
6073 let err = stream
6074 .message()
6075 .await
6076 .expect_err("EOF without grpc-status must surface as Err");
6077 assert_eq!(err.code, ErrorCode::Internal);
6078 assert!(
6079 err.to_string().contains("grpc-status"),
6080 "unexpected error: {err}"
6081 );
6082
6083 let again = stream
6084 .message()
6085 .await
6086 .expect_err("missing-status error must be sticky");
6087 assert_eq!(again.code, ErrorCode::Internal);
6088 }
6089
6090 #[tokio::test]
6095 async fn grpc_header_status_does_not_excuse_truncation_after_data() {
6096 use buffa::Message;
6097 use buffa_types::google::protobuf::__buffa::view::StringValueView;
6098 use buffa_types::google::protobuf::StringValue;
6099
6100 let mut headers = http::HeaderMap::new();
6101 headers.insert("grpc-status", "0".parse().unwrap());
6102 let body = Full::new(Envelope::data(StringValue::from("hello").encode_to_bytes()).encode());
6103 let mut stream: ServerStream<_, StringValueView<'static>> = ServerStream {
6104 headers,
6105 body,
6106 buf: BytesMut::new(),
6107 encoding: None,
6108 compression: CompressionRegistry::new(),
6109 codec_format: CodecFormat::Proto,
6110 protocol: Protocol::Grpc,
6111 max_message_size: Some(1024),
6112 deadline: None,
6113 end: None,
6114 saw_body_data: false,
6115 _phantom: PhantomData,
6116 };
6117
6118 let msg = stream
6119 .message()
6120 .await
6121 .expect("data envelope should decode")
6122 .expect("stream should yield the data message first");
6123 assert_eq!(msg.view().value, "hello");
6124
6125 let err = stream
6126 .message()
6127 .await
6128 .expect_err("truncation after data must surface as Err despite header status");
6129 assert_eq!(err.code, ErrorCode::Internal);
6130 }
6131
6132 #[tokio::test]
6137 async fn grpc_trailers_only_ok_response_ends_cleanly() {
6138 use buffa_types::google::protobuf::__buffa::view::StringValueView;
6139
6140 let mut headers = http::HeaderMap::new();
6141 headers.insert("grpc-status", "0".parse().unwrap());
6142 let mut stream: ServerStream<_, StringValueView<'static>> = ServerStream {
6143 headers,
6144 body: Full::new(Bytes::new()),
6145 buf: BytesMut::new(),
6146 encoding: None,
6147 compression: CompressionRegistry::new(),
6148 codec_format: CodecFormat::Proto,
6149 protocol: Protocol::Grpc,
6150 max_message_size: Some(1024),
6151 deadline: None,
6152 end: None,
6153 saw_body_data: false,
6154 _phantom: PhantomData,
6155 };
6156
6157 assert!(stream.message().await.unwrap().is_none());
6158 assert!(stream.error().is_none());
6159 assert!(stream.message().await.unwrap().is_none());
6161 }
6162
6163 #[tokio::test]
6167 async fn grpc_trailers_without_status_errors() {
6168 use buffa_types::google::protobuf::__buffa::view::StringValueView;
6169 use http_body::Frame;
6170 use http_body_util::StreamBody;
6171
6172 let mut trailers = http::HeaderMap::new();
6173 trailers.insert("x-meta", "1".parse().unwrap());
6174 let frames: Vec<Result<Frame<Bytes>, std::convert::Infallible>> =
6175 vec![Ok(Frame::trailers(trailers))];
6176 let body = StreamBody::new(futures::stream::iter(frames));
6177
6178 let mut stream: ServerStream<_, StringValueView<'static>> = ServerStream {
6179 headers: http::HeaderMap::new(),
6180 body,
6181 buf: BytesMut::new(),
6182 encoding: None,
6183 compression: CompressionRegistry::new(),
6184 codec_format: CodecFormat::Proto,
6185 protocol: Protocol::Grpc,
6186 max_message_size: Some(1024),
6187 deadline: None,
6188 end: None,
6189 saw_body_data: false,
6190 _phantom: PhantomData,
6191 };
6192
6193 let err = stream
6194 .message()
6195 .await
6196 .expect_err("trailers without grpc-status must surface as Err");
6197 assert_eq!(err.code, ErrorCode::Unknown);
6200 assert!(stream.trailers().is_some());
6202
6203 let again = stream
6204 .message()
6205 .await
6206 .expect_err("missing-status error must be sticky");
6207 assert_eq!(again.code, ErrorCode::Unknown);
6208 }
6209
6210 #[tokio::test(start_paused = true)]
6218 async fn deadline_bounds_multi_frame_message() {
6219 use std::time::Duration;
6220
6221 use buffa_types::google::protobuf::__buffa::view::StringValueView;
6222 use http_body::Frame;
6223 use http_body_util::StreamBody;
6224
6225 let frames: std::pin::Pin<
6229 Box<dyn futures::Stream<Item = Result<Frame<Bytes>, std::convert::Infallible>> + Send>,
6230 > = Box::pin(futures::stream::unfold(0u32, |n| async move {
6231 if n >= 32 {
6232 return None;
6233 }
6234 tokio::time::sleep(Duration::from_millis(40)).await;
6235 Some((
6236 Ok::<_, std::convert::Infallible>(Frame::data(Bytes::from_static(&[0u8]))),
6237 n + 1,
6238 ))
6239 }));
6240 let body = StreamBody::new(frames);
6241
6242 let mut stream: ServerStream<_, StringValueView<'static>> = ServerStream {
6243 headers: http::HeaderMap::new(),
6244 body,
6245 buf: BytesMut::new(),
6246 encoding: None,
6247 compression: CompressionRegistry::new(),
6248 codec_format: CodecFormat::Proto,
6249 protocol: Protocol::Grpc,
6250 max_message_size: Some(1024),
6251 deadline: Some(std::time::Instant::now() + Duration::from_millis(100)),
6252 end: None,
6253 saw_body_data: false,
6254 _phantom: PhantomData,
6255 };
6256
6257 let start = tokio::time::Instant::now();
6258 let err = stream
6259 .message()
6260 .await
6261 .expect_err("absolute deadline must fire mid-trickle");
6262 assert_eq!(err.code, ErrorCode::DeadlineExceeded);
6263 assert!(
6264 start.elapsed() >= Duration::from_millis(100),
6265 "deadline must not fire early (elapsed {:?})",
6266 start.elapsed()
6267 );
6268 assert!(
6269 start.elapsed() <= Duration::from_millis(150),
6270 "deadline must be absolute across polls, not per-poll relative (elapsed {:?})",
6271 start.elapsed()
6272 );
6273
6274 let again = stream
6275 .message()
6276 .await
6277 .expect_err("deadline error is sticky");
6278 assert_eq!(again.code, ErrorCode::DeadlineExceeded);
6279 }
6280
6281 #[tokio::test]
6285 async fn grpc_malformed_status_errors() {
6286 use buffa_types::google::protobuf::__buffa::view::StringValueView;
6287 use http_body::Frame;
6288 use http_body_util::StreamBody;
6289
6290 let mut trailers = http::HeaderMap::new();
6291 trailers.insert("grpc-status", "banana".parse().unwrap());
6292 let frames: Vec<Result<Frame<Bytes>, std::convert::Infallible>> =
6293 vec![Ok(Frame::trailers(trailers))];
6294 let body = StreamBody::new(futures::stream::iter(frames));
6295
6296 let mut stream: ServerStream<_, StringValueView<'static>> = ServerStream {
6297 headers: http::HeaderMap::new(),
6298 body,
6299 buf: BytesMut::new(),
6300 encoding: None,
6301 compression: CompressionRegistry::new(),
6302 codec_format: CodecFormat::Proto,
6303 protocol: Protocol::Grpc,
6304 max_message_size: Some(1024),
6305 deadline: None,
6306 end: None,
6307 saw_body_data: false,
6308 _phantom: PhantomData,
6309 };
6310
6311 let err = stream
6312 .message()
6313 .await
6314 .expect_err("malformed grpc-status must surface as Err");
6315 assert_eq!(err.code, ErrorCode::Unknown);
6316 assert!(
6317 err.to_string().contains("malformed grpc-status"),
6318 "unexpected error: {err}"
6319 );
6320
6321 let again = stream
6322 .message()
6323 .await
6324 .expect_err("malformed-status error must be sticky");
6325 assert_eq!(again.code, ErrorCode::Unknown);
6326 }
6327
6328 #[cfg(feature = "client")]
6329 fn plaintext_client_with_https_base() -> (HttpClient, ClientConfig) {
6330 let client = HttpClient::plaintext();
6331 let config = ClientConfig::new("https://localhost:8080".parse().unwrap());
6332 (client, config)
6333 }
6334
6335 #[test]
6336 fn transport_send_error_mapper_preserves_connect_error_in_source_chain() {
6337 #[derive(Debug)]
6338 struct WrappedTransportError(ConnectError);
6339
6340 impl std::fmt::Display for WrappedTransportError {
6341 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
6342 write!(f, "wrapped transport failure")
6343 }
6344 }
6345
6346 impl std::error::Error for WrappedTransportError {
6347 fn source(&self) -> Option<&(dyn std::error::Error + 'static)> {
6348 Some(&self.0)
6349 }
6350 }
6351
6352 let mapped = map_transport_send_error(
6353 WrappedTransportError(ConnectError::invalid_argument("bad client config")),
6354 "request failed",
6355 );
6356 assert_eq!(mapped.code, ErrorCode::InvalidArgument);
6357 assert_eq!(mapped.message.as_deref(), Some("bad client config"));
6358 }
6359
6360 #[test]
6361 fn transport_send_error_mapper_preserves_source_when_no_connect_error_in_chain() {
6362 #[derive(Debug)]
6363 struct PlainTransportError;
6364
6365 impl std::fmt::Display for PlainTransportError {
6366 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
6367 write!(f, "connection reset by peer")
6368 }
6369 }
6370
6371 impl std::error::Error for PlainTransportError {}
6372
6373 let mapped = map_transport_send_error(PlainTransportError, "request failed");
6374 assert_eq!(mapped.code, ErrorCode::Unavailable);
6375 let source = std::error::Error::source(&mapped).expect("source must be preserved");
6376 assert_eq!(source.to_string(), "connection reset by peer");
6377 }
6378
6379 #[cfg(feature = "client")]
6380 #[tokio::test]
6381 async fn call_unary_preserves_transport_connect_error() {
6382 use buffa_types::google::protobuf::__buffa::view::StringValueView;
6383 use buffa_types::google::protobuf::StringValue;
6384
6385 let (client, config) = plaintext_client_with_https_base();
6386 let err = call_unary::<_, StringValue, StringValueView<'static>>(
6387 &client,
6388 &config,
6389 "test.Service",
6390 "Unary",
6391 StringValue::from("hello"),
6392 CallOptions::default(),
6393 )
6394 .await
6395 .expect_err("transport config error must surface from unary call");
6396 assert_eq!(err.code, ErrorCode::InvalidArgument);
6397 assert!(err.message.as_deref().unwrap().contains("with_tls"));
6398 }
6399
6400 #[cfg(feature = "client")]
6401 #[tokio::test]
6402 async fn call_unary_get_preserves_transport_connect_error() {
6403 use buffa_types::google::protobuf::__buffa::view::StringValueView;
6404 use buffa_types::google::protobuf::StringValue;
6405
6406 let (client, config) = plaintext_client_with_https_base();
6407 let err = call_unary_get::<_, StringValue, StringValueView<'static>>(
6408 &client,
6409 &config,
6410 "test.Service",
6411 "UnaryGet",
6412 StringValue::from("hello"),
6413 CallOptions::default(),
6414 )
6415 .await
6416 .expect_err("transport config error must surface from unary GET");
6417 assert_eq!(err.code, ErrorCode::InvalidArgument);
6418 assert!(err.message.as_deref().unwrap().contains("with_tls"));
6419 }
6420
6421 #[cfg(feature = "client")]
6422 #[tokio::test]
6423 async fn call_server_stream_preserves_transport_connect_error() {
6424 use buffa_types::google::protobuf::__buffa::view::StringValueView;
6425 use buffa_types::google::protobuf::StringValue;
6426
6427 let (client, config) = plaintext_client_with_https_base();
6428 let err = call_server_stream::<_, StringValue, StringValueView<'static>>(
6429 &client,
6430 &config,
6431 "test.Service",
6432 "ServerStream",
6433 StringValue::from("hello"),
6434 CallOptions::default(),
6435 )
6436 .await
6437 .expect_err("transport config error must surface from server stream");
6438 assert_eq!(err.code, ErrorCode::InvalidArgument);
6439 assert!(err.message.as_deref().unwrap().contains("with_tls"));
6440 }
6441
6442 #[cfg(feature = "client")]
6443 #[tokio::test]
6444 async fn call_bidi_stream_preserves_transport_connect_error() {
6445 use buffa_types::google::protobuf::__buffa::view::StringValueView;
6446 use buffa_types::google::protobuf::StringValue;
6447
6448 let (client, config) = plaintext_client_with_https_base();
6449 let mut stream = call_bidi_stream::<_, StringValue, StringValueView<'static>>(
6450 &client,
6451 &config,
6452 "test.Service",
6453 "Bidi",
6454 CallOptions::default(),
6455 )
6456 .await
6457 .expect("constructing bidi stream should succeed until the first receive");
6458 let err = stream
6459 .message()
6460 .await
6461 .expect_err("transport config error must surface from bidi receive");
6462 assert_eq!(err.code, ErrorCode::InvalidArgument);
6463 assert!(err.message.as_deref().unwrap().contains("with_tls"));
6464 assert_eq!(
6465 stream.error().map(|e| e.code),
6466 Some(ErrorCode::InvalidArgument)
6467 );
6468 }
6469
6470 #[cfg(feature = "client")]
6471 #[tokio::test]
6472 async fn call_client_stream_preserves_transport_connect_error() {
6473 use buffa_types::google::protobuf::__buffa::view::StringValueView;
6474 use buffa_types::google::protobuf::StringValue;
6475
6476 let (client, config) = plaintext_client_with_https_base();
6477 let err = call_client_stream::<_, StringValue, StringValueView<'static>>(
6478 &client,
6479 &config,
6480 "test.Service",
6481 "ClientStream",
6482 futures::stream::iter([StringValue::from("hello")]),
6483 CallOptions::default(),
6484 )
6485 .await
6486 .expect_err("transport config error must surface from client stream");
6487 assert_eq!(err.code, ErrorCode::InvalidArgument);
6488 assert!(err.message.as_deref().unwrap().contains("with_tls"));
6489 }
6490
6491 #[cfg(feature = "client")]
6496 #[derive(Clone)]
6497 struct PendingSendTransport {
6498 started: std::sync::Arc<std::sync::Mutex<Option<tokio::sync::oneshot::Sender<()>>>>,
6499 dropped: std::sync::Arc<std::sync::Mutex<Option<tokio::sync::oneshot::Sender<()>>>>,
6500 }
6501
6502 #[cfg(feature = "client")]
6503 struct PendingSendFuture {
6504 _request: Request<ClientBody>,
6508 started: Option<tokio::sync::oneshot::Sender<()>>,
6509 dropped: Option<tokio::sync::oneshot::Sender<()>>,
6510 }
6511
6512 #[cfg(feature = "client")]
6513 impl std::future::Future for PendingSendFuture {
6514 type Output = Result<Response<Full<Bytes>>, std::io::Error>;
6515
6516 fn poll(
6517 mut self: std::pin::Pin<&mut Self>,
6518 _cx: &mut std::task::Context<'_>,
6519 ) -> std::task::Poll<Self::Output> {
6520 if let Some(tx) = self.started.take() {
6521 let _ = tx.send(());
6522 }
6523 std::task::Poll::Pending
6524 }
6525 }
6526
6527 #[cfg(feature = "client")]
6528 impl Drop for PendingSendFuture {
6529 fn drop(&mut self) {
6530 if let Some(tx) = self.dropped.take() {
6531 let _ = tx.send(());
6532 }
6533 }
6534 }
6535
6536 #[cfg(feature = "client")]
6537 impl ClientTransport for PendingSendTransport {
6538 type ResponseBody = Full<Bytes>;
6539 type Error = std::io::Error;
6540
6541 fn send(
6542 &self,
6543 request: Request<ClientBody>,
6544 ) -> BoxFuture<'static, Result<Response<Self::ResponseBody>, Self::Error>> {
6545 let started = Some(
6550 self.started
6551 .lock()
6552 .unwrap()
6553 .take()
6554 .expect("PendingSendTransport::send called more than once"),
6555 );
6556 let dropped = Some(
6557 self.dropped
6558 .lock()
6559 .unwrap()
6560 .take()
6561 .expect("PendingSendTransport::send called more than once"),
6562 );
6563 Box::pin(PendingSendFuture {
6564 _request: request,
6565 started,
6566 dropped,
6567 })
6568 }
6569 }
6570
6571 #[cfg(feature = "client")]
6572 fn pending_send_transport() -> (
6573 PendingSendTransport,
6574 tokio::sync::oneshot::Receiver<()>,
6575 tokio::sync::oneshot::Receiver<()>,
6576 ) {
6577 let (started_tx, started_rx) = tokio::sync::oneshot::channel::<()>();
6578 let (dropped_tx, dropped_rx) = tokio::sync::oneshot::channel::<()>();
6579 let transport = PendingSendTransport {
6580 started: std::sync::Arc::new(std::sync::Mutex::new(Some(started_tx))),
6581 dropped: std::sync::Arc::new(std::sync::Mutex::new(Some(dropped_tx))),
6582 };
6583 (transport, started_rx, dropped_rx)
6584 }
6585
6586 #[cfg(feature = "client")]
6590 #[tokio::test(start_paused = true)]
6591 async fn client_stream_deadline_drops_transport_send() {
6592 use buffa_types::google::protobuf::__buffa::view::StringValueView;
6593 use buffa_types::google::protobuf::StringValue;
6594
6595 let (transport, started_rx, dropped_rx) = pending_send_transport();
6596 let config = ClientConfig::new("http://localhost:8080".parse().unwrap());
6597
6598 let mut call = Box::pin(
6599 call_client_stream::<_, StringValue, StringValueView<'static>>(
6600 &transport,
6601 &config,
6602 "test.Service",
6603 "ClientStream",
6604 futures::stream::empty::<StringValue>(),
6605 CallOptions::default().with_timeout(Duration::from_millis(100)),
6606 ),
6607 );
6608
6609 tokio::select! {
6613 res = &mut call => panic!("call resolved before the transport was polled: {res:?}"),
6614 started = started_rx => started.expect("transport send future was never polled"),
6615 }
6616
6617 let err = call
6619 .await
6620 .expect_err("deadline must fire while the transport waits for headers");
6621 assert_eq!(err.code, ErrorCode::DeadlineExceeded);
6622
6623 tokio::time::timeout(Duration::from_secs(5), dropped_rx)
6626 .await
6627 .expect("transport send future was not dropped after the deadline fired")
6628 .expect("drop signal sender vanished without firing");
6629 }
6630
6631 #[cfg(feature = "client")]
6637 #[tokio::test]
6638 async fn client_stream_cancellation_drops_transport_send() {
6639 use buffa_types::google::protobuf::__buffa::view::StringValueView;
6640 use buffa_types::google::protobuf::StringValue;
6641
6642 let (transport, started_rx, dropped_rx) = pending_send_transport();
6643 let config = ClientConfig::new("http://localhost:8080".parse().unwrap());
6644
6645 let mut call = Box::pin(
6646 call_client_stream::<_, StringValue, StringValueView<'static>>(
6647 &transport,
6648 &config,
6649 "test.Service",
6650 "ClientStream",
6651 futures::stream::empty::<StringValue>(),
6652 CallOptions::default(),
6653 ),
6654 );
6655
6656 tokio::select! {
6660 _ = &mut call => panic!("call completed though the transport never responded"),
6661 started = started_rx => started.expect("transport send future was never polled"),
6662 }
6663 drop(call);
6664
6665 tokio::time::timeout(Duration::from_secs(5), dropped_rx)
6666 .await
6667 .expect("transport send future was not dropped after caller cancellation")
6668 .expect("drop signal sender vanished without firing");
6669 }
6670
6671 #[cfg(feature = "client")]
6677 #[tokio::test(start_paused = true)]
6678 async fn client_stream_deadline_drops_send_with_unread_request_body() {
6679 use buffa_types::google::protobuf::__buffa::view::StringValueView;
6680 use buffa_types::google::protobuf::StringValue;
6681
6682 let (transport, started_rx, dropped_rx) = pending_send_transport();
6683 let config = ClientConfig::new("http://localhost:8080".parse().unwrap());
6684
6685 let requests: Vec<StringValue> = (0..256)
6687 .map(|i| StringValue::from(format!("m{i}")))
6688 .collect();
6689
6690 let mut call = Box::pin(
6691 call_client_stream::<_, StringValue, StringValueView<'static>>(
6692 &transport,
6693 &config,
6694 "test.Service",
6695 "ClientStream",
6696 stream_iter(requests),
6697 CallOptions::default().with_timeout(Duration::from_millis(100)),
6698 ),
6699 );
6700
6701 tokio::select! {
6704 res = &mut call => panic!("call resolved before the transport was polled: {res:?}"),
6705 started = started_rx => started.expect("transport send future was never polled"),
6706 }
6707
6708 let err = call
6709 .await
6710 .expect_err("deadline must fire while the upload is unfinished");
6711 assert_eq!(err.code, ErrorCode::DeadlineExceeded);
6712
6713 tokio::time::timeout(Duration::from_secs(5), dropped_rx)
6714 .await
6715 .expect("transport send future was not dropped despite the unread request body")
6716 .expect("drop signal sender vanished without firing");
6717 }
6718
6719 #[cfg(feature = "client")]
6722 #[tokio::test]
6723 async fn client_stream_returns_well_formed_response() {
6724 use buffa::Message;
6725 use buffa_types::google::protobuf::__buffa::view::StringValueView;
6726 use buffa_types::google::protobuf::StringValue;
6727
6728 #[derive(Clone)]
6729 struct FixedResponseTransport {
6730 body: Bytes,
6731 }
6732
6733 impl ClientTransport for FixedResponseTransport {
6734 type ResponseBody = Full<Bytes>;
6735 type Error = std::io::Error;
6736
6737 fn send(
6738 &self,
6739 _request: Request<ClientBody>,
6740 ) -> BoxFuture<'static, Result<Response<Self::ResponseBody>, Self::Error>> {
6741 let body = self.body.clone();
6742 Box::pin(async move {
6743 let response = Response::builder()
6744 .status(http::StatusCode::OK)
6745 .header(http::header::CONTENT_TYPE, "application/connect+proto")
6746 .body(Full::new(body))
6747 .unwrap();
6748 Ok(response)
6749 })
6750 }
6751 }
6752
6753 let data =
6757 crate::envelope::Envelope::data(Bytes::from(StringValue::from("ok").encode_to_vec()))
6758 .encode();
6759 let end = crate::envelope::Envelope::end_stream(Bytes::from_static(b"{}")).encode();
6760 let mut body = BytesMut::new();
6761 body.extend_from_slice(&data);
6762 body.extend_from_slice(&end);
6763 let transport = FixedResponseTransport {
6764 body: body.freeze(),
6765 };
6766 let config = ClientConfig::new("http://localhost:8080".parse().unwrap());
6767
6768 let response = call_client_stream::<_, StringValue, StringValueView<'static>>(
6769 &transport,
6770 &config,
6771 "test.Service",
6772 "ClientStream",
6773 stream_iter([StringValue::from("req")]),
6774 CallOptions::default(),
6775 )
6776 .await
6777 .expect("well-formed client-streaming response must decode");
6778 assert_eq!(response.view().value, "ok");
6779 }
6780
6781 #[cfg(feature = "client")]
6784 #[tokio::test]
6785 async fn client_stream_transport_send_error_still_surfaces() {
6786 use buffa_types::google::protobuf::__buffa::view::StringValueView;
6787 use buffa_types::google::protobuf::StringValue;
6788
6789 #[derive(Clone)]
6790 struct FailingTransport;
6791
6792 impl ClientTransport for FailingTransport {
6793 type ResponseBody = Full<Bytes>;
6794 type Error = std::io::Error;
6795
6796 fn send(
6797 &self,
6798 _request: Request<ClientBody>,
6799 ) -> BoxFuture<'static, Result<Response<Self::ResponseBody>, Self::Error>> {
6800 Box::pin(async { Err(std::io::Error::other("boom")) })
6801 }
6802 }
6803
6804 let config = ClientConfig::new("http://localhost:8080".parse().unwrap());
6805 let err = call_client_stream::<_, StringValue, StringValueView<'static>>(
6806 &FailingTransport,
6807 &config,
6808 "test.Service",
6809 "ClientStream",
6810 stream_iter([StringValue::from("req")]),
6811 CallOptions::default(),
6812 )
6813 .await
6814 .expect_err("transport send failure must surface");
6815 assert_eq!(err.code, ErrorCode::Unavailable);
6816 let message = err.message.as_deref().unwrap_or_default();
6817 assert!(message.contains("request failed"), "unexpected: {message}");
6818 assert!(message.contains("boom"), "unexpected: {message}");
6819 }
6820
6821 #[cfg(feature = "client")]
6822 #[tokio::test]
6823 async fn http_client_plaintext_rejects_https() {
6824 let client = HttpClient::plaintext();
6825 let req = Request::builder()
6826 .uri("https://localhost:8080/foo")
6827 .body(full_body(Bytes::new()))
6828 .unwrap();
6829 let err = client.send(req).await.unwrap_err();
6830 assert_eq!(err.code, ErrorCode::InvalidArgument);
6831 assert!(err.message.as_deref().unwrap().contains("with_tls"));
6832 }
6833
6834 #[cfg(all(feature = "client", feature = "client-tls"))]
6835 #[tokio::test]
6836 async fn http_client_with_tls_rejects_http() {
6837 let tls_config = std::sync::Arc::new(
6838 rustls::ClientConfig::builder()
6839 .with_root_certificates(rustls::RootCertStore::empty())
6840 .with_no_client_auth(),
6841 );
6842 let client = HttpClient::with_tls(tls_config);
6843 let req = Request::builder()
6844 .uri("http://localhost:8080/foo")
6845 .body(full_body(Bytes::new()))
6846 .unwrap();
6847 let err = client.send(req).await.unwrap_err();
6848 assert_eq!(err.code, ErrorCode::InvalidArgument);
6849 assert!(err.message.as_deref().unwrap().contains("plaintext"));
6850 }
6851
6852 #[cfg(all(feature = "client", feature = "client-tls"))]
6853 #[test]
6854 fn http_client_with_tls_construction() {
6855 let tls_config = std::sync::Arc::new(
6858 rustls::ClientConfig::builder()
6859 .with_root_certificates(rustls::RootCertStore::empty())
6860 .with_no_client_auth(),
6861 );
6862 let _client = HttpClient::with_tls(tls_config);
6863 }
6864
6865 #[test]
6870 fn test_format_timeout_connect() {
6871 assert_eq!(
6872 format_timeout(Duration::from_millis(5000), Protocol::Connect),
6873 "5000"
6874 );
6875 assert_eq!(
6876 format_timeout(Duration::from_secs(0), Protocol::Connect),
6877 "0"
6878 );
6879 }
6880
6881 #[test]
6882 fn test_format_timeout_connect_caps_at_10_digits() {
6883 assert_eq!(
6885 format_timeout(Duration::from_millis(9_999_999_999), Protocol::Connect),
6886 "9999999999"
6887 );
6888 assert_eq!(
6893 format_timeout(Duration::from_secs(365 * 86400), Protocol::Connect),
6894 "9999999999"
6895 );
6896 assert_eq!(
6897 format_timeout(Duration::MAX, Protocol::Connect),
6898 "9999999999"
6899 );
6900 }
6901
6902 #[test]
6903 fn connect_huge_timeout_clamps_before_deadline() {
6904 let encoded = encoded_timeout(Duration::MAX, Protocol::Connect);
6905 assert_eq!(encoded.header_value(), "9999999999");
6906 assert_eq!(
6907 encoded.duration(),
6908 Duration::from_millis(CONNECT_TIMEOUT_MAX_MILLIS)
6909 );
6910 assert!(client_deadline(Some(Duration::MAX), Protocol::Connect).is_some());
6911 }
6912
6913 #[test]
6914 fn test_format_timeout_grpc_seconds() {
6915 assert_eq!(
6916 format_timeout(Duration::from_secs(30), Protocol::Grpc),
6917 "30S"
6918 );
6919 }
6920
6921 #[test]
6922 fn test_format_timeout_grpc_milliseconds() {
6923 assert_eq!(
6924 format_timeout(Duration::from_millis(500), Protocol::Grpc),
6925 "500m"
6926 );
6927 }
6928
6929 #[test]
6930 fn test_format_timeout_grpc_microseconds() {
6931 assert_eq!(
6932 format_timeout(Duration::from_micros(100), Protocol::Grpc),
6933 "100u"
6934 );
6935 }
6936
6937 #[test]
6938 fn test_format_timeout_grpc_nanoseconds() {
6939 assert_eq!(
6940 format_timeout(Duration::from_nanos(999), Protocol::Grpc),
6941 "999n"
6942 );
6943 }
6944
6945 #[test]
6946 fn test_format_timeout_grpc_zero() {
6947 assert_eq!(format_timeout(Duration::from_secs(0), Protocol::Grpc), "0n");
6948 }
6949
6950 #[test]
6951 fn test_format_timeout_grpc_8_digit_limit() {
6952 assert_eq!(
6954 format_timeout(Duration::from_secs(99_999_999), Protocol::Grpc),
6955 "99999999S"
6956 );
6957 assert_eq!(
6959 format_timeout(Duration::from_secs(100_000_000), Protocol::Grpc),
6960 "99999999S"
6961 );
6962 }
6963
6964 #[test]
6965 fn grpc_huge_timeout_clamps_before_deadline() {
6966 let encoded = encoded_timeout(Duration::MAX, Protocol::Grpc);
6967 assert_eq!(encoded.header_value(), "99999999S");
6968 assert_eq!(
6969 encoded.duration(),
6970 Duration::from_secs(GRPC_TIMEOUT_MAX_SECONDS)
6971 );
6972 assert!(client_deadline(Some(Duration::MAX), Protocol::Grpc).is_some());
6973 }
6974
6975 #[test]
6976 fn test_format_timeout_grpc_web_same_as_grpc() {
6977 assert_eq!(
6978 format_timeout(Duration::from_millis(500), Protocol::GrpcWeb),
6979 "500m"
6980 );
6981 }
6982
6983 #[test]
6984 fn test_format_timeout_grpc_subsecond_nanosecond_residue() {
6985 assert_eq!(
6989 format_timeout(Duration::from_nanos(100_000_001), Protocol::Grpc),
6990 "100000u" );
6992 let encoded = encoded_timeout(Duration::from_nanos(100_000_001), Protocol::Grpc);
6993 assert_eq!(encoded.duration(), Duration::from_micros(100_000));
6994 assert_eq!(
6996 format_timeout(Duration::from_nanos(100_000_000), Protocol::Grpc),
6997 "100m" );
6999 assert_eq!(
7001 format_timeout(Duration::from_nanos(200_000_000_001), Protocol::Grpc),
7002 "200000m" );
7004 }
7005
7006 #[test]
7011 fn test_grpc_percent_decode_passthrough() {
7012 assert_eq!(grpc_percent_decode("hello world"), "hello world");
7013 }
7014
7015 #[test]
7016 fn test_grpc_percent_decode_percent() {
7017 assert_eq!(grpc_percent_decode("100%25"), "100%");
7018 }
7019
7020 #[test]
7021 fn test_grpc_percent_decode_newlines() {
7022 assert_eq!(grpc_percent_decode("a%0Ab"), "a\nb");
7023 assert_eq!(grpc_percent_decode("a%0D%0Ab"), "a\r\nb");
7024 }
7025
7026 #[test]
7027 fn test_grpc_percent_decode_utf8_multibyte() {
7028 assert_eq!(grpc_percent_decode("caf%C3%A9"), "café");
7030 assert_eq!(grpc_percent_decode("%E2%98%BA"), "☺");
7032 assert_eq!(grpc_percent_decode("%F0%9F%98%88"), "😈");
7034 }
7035
7036 #[test]
7037 fn test_grpc_percent_decode_partial_percent() {
7038 assert_eq!(grpc_percent_decode("100%"), "100%");
7040 assert_eq!(grpc_percent_decode("a%2"), "a%2");
7041 }
7042
7043 #[test]
7044 fn test_grpc_percent_decode_invalid_hex() {
7045 assert_eq!(grpc_percent_decode("a%ZZb"), "a%ZZb");
7046 }
7047
7048 #[test]
7053 fn test_parse_grpc_error_ok_returns_none() {
7054 let mut trailers = http::HeaderMap::new();
7055 trailers.insert("grpc-status", http::HeaderValue::from_static("0"));
7056 assert!(parse_grpc_error_from_trailers(&trailers).is_none());
7057 }
7058
7059 #[test]
7060 fn test_parse_grpc_error_missing_status_returns_none() {
7061 let trailers = http::HeaderMap::new();
7062 assert!(parse_grpc_error_from_trailers(&trailers).is_none());
7063 }
7064
7065 #[test]
7066 fn test_parse_grpc_error_with_code_and_message() {
7067 let mut trailers = http::HeaderMap::new();
7068 trailers.insert("grpc-status", http::HeaderValue::from_static("5"));
7069 trailers.insert(
7070 "grpc-message",
7071 http::HeaderValue::from_static("not%20found"),
7072 );
7073 let err = parse_grpc_error_from_trailers(&trailers).unwrap();
7074 assert_eq!(err.code, ErrorCode::NotFound);
7075 assert_eq!(err.message.as_deref(), Some("not found"));
7076 }
7077
7078 #[test]
7079 fn test_parse_grpc_error_unknown_code() {
7080 let mut trailers = http::HeaderMap::new();
7081 trailers.insert("grpc-status", http::HeaderValue::from_static("99"));
7082 let err = parse_grpc_error_from_trailers(&trailers).unwrap();
7083 assert_eq!(err.code, ErrorCode::Unknown);
7084 }
7085
7086 #[test]
7087 fn test_parse_grpc_error_custom_trailers() {
7088 let mut trailers = http::HeaderMap::new();
7089 trailers.insert("grpc-status", http::HeaderValue::from_static("13"));
7090 trailers.insert("x-custom", http::HeaderValue::from_static("value"));
7091 let err = parse_grpc_error_from_trailers(&trailers).unwrap();
7092 assert_eq!(err.code, ErrorCode::Internal);
7093 assert_eq!(
7094 err.trailers().get("x-custom").unwrap().to_str().unwrap(),
7095 "value"
7096 );
7097 }
7098
7099 #[test]
7106 fn test_parse_grpc_web_trailer_uncompressed() {
7107 let payload = b"grpc-status: 0\r\n";
7108 let mut frame = Vec::with_capacity(5 + payload.len());
7109 frame.push(0x80);
7110 frame.extend_from_slice(&(payload.len() as u32).to_be_bytes());
7111 frame.extend_from_slice(payload);
7112
7113 let headers = parse_grpc_web_trailer_frame_with_compression(&frame, None).unwrap();
7114 assert_eq!(headers.get("grpc-status").unwrap().to_str().unwrap(), "0");
7115 }
7116
7117 #[test]
7118 fn test_parse_grpc_web_trailer_with_error() {
7119 let payload = b"grpc-status: 13\r\ngrpc-message: internal error\r\n";
7120 let mut frame = Vec::with_capacity(5 + payload.len());
7121 frame.push(0x80);
7122 frame.extend_from_slice(&(payload.len() as u32).to_be_bytes());
7123 frame.extend_from_slice(payload);
7124
7125 let headers = parse_grpc_web_trailer_frame_with_compression(&frame, None).unwrap();
7126 assert_eq!(headers.get("grpc-status").unwrap().to_str().unwrap(), "13");
7127 assert_eq!(
7128 headers.get("grpc-message").unwrap().to_str().unwrap(),
7129 "internal error"
7130 );
7131 }
7132
7133 #[test]
7134 fn test_parse_grpc_web_trailer_truncated() {
7135 assert!(parse_grpc_web_trailer_frame_with_compression(&[0x80, 0, 0], None).is_none());
7137 }
7138
7139 #[test]
7140 fn test_parse_grpc_web_trailer_not_trailer() {
7141 let frame = [0x00, 0, 0, 0, 5, b'h', b'e', b'l', b'l', b'o'];
7143 assert!(parse_grpc_web_trailer_frame_with_compression(&frame, None).is_none());
7144 }
7145
7146 #[test]
7147 fn test_parse_grpc_web_trailer_compressed_no_registry() {
7148 let payload = b"grpc-status: 0\r\n";
7150 let mut frame = Vec::with_capacity(5 + payload.len());
7151 frame.push(0x81);
7152 frame.extend_from_slice(&(payload.len() as u32).to_be_bytes());
7153 frame.extend_from_slice(payload);
7154 assert!(parse_grpc_web_trailer_frame_with_compression(&frame, None).is_none());
7156 }
7157
7158 #[test]
7159 fn test_parse_grpc_web_trailer_floods_do_not_panic() {
7160 let mut payload = String::new();
7165 for i in 0..40_000u32 {
7166 payload.push_str(&format!("h{i}:\n"));
7167 }
7168 let payload = payload.into_bytes();
7169 assert!(
7170 payload.len() < 1024 * 1024,
7171 "payload must stay under byte cap"
7172 );
7173
7174 let mut frame = Vec::with_capacity(5 + payload.len());
7175 frame.push(0x80);
7176 frame.extend_from_slice(&(payload.len() as u32).to_be_bytes());
7177 frame.extend_from_slice(&payload);
7178
7179 let headers = parse_grpc_web_trailer_frame_with_compression(&frame, None)
7181 .expect("flood frame is well-formed and should parse");
7182 assert!(headers.keys_len() > 0, "early trailers should be retained");
7185 assert!(headers.keys_len() <= 1 << 15);
7186 }
7187
7188 #[test]
7189 fn test_append_metadata_capped_copies_entries() {
7190 let mut metadata = HashMap::new();
7191 metadata.insert("grpc-status".to_string(), vec!["0".to_string()]);
7192 metadata.insert(
7193 "x-custom".to_string(),
7194 vec!["a".to_string(), "b".to_string()],
7195 );
7196 let mut trailers = http::HeaderMap::new();
7197 append_metadata_capped(&mut trailers, metadata);
7198 assert_eq!(trailers.get("grpc-status").unwrap(), "0");
7199 assert_eq!(trailers.get_all("x-custom").iter().count(), 2);
7200 }
7201
7202 #[test]
7203 fn test_append_metadata_capped_does_not_panic_on_flood() {
7204 let mut metadata = HashMap::new();
7209 for i in 0..40_000u32 {
7210 metadata.insert(format!("h{i}"), vec![String::new()]);
7211 }
7212 let mut trailers = http::HeaderMap::new();
7213 append_metadata_capped(&mut trailers, metadata);
7214 assert!(trailers.keys_len() > 0, "early entries should be retained");
7215 assert!(trailers.keys_len() <= 1 << 15);
7216 }
7217
7218 #[test]
7219 fn test_parse_grpc_web_trailer_newline_only() {
7220 let payload = b"grpc-status: 0\n";
7222 let mut frame = Vec::with_capacity(5 + payload.len());
7223 frame.push(0x80);
7224 frame.extend_from_slice(&(payload.len() as u32).to_be_bytes());
7225 frame.extend_from_slice(payload);
7226
7227 let headers = parse_grpc_web_trailer_frame_with_compression(&frame, None).unwrap();
7228 assert_eq!(headers.get("grpc-status").unwrap().to_str().unwrap(), "0");
7229 }
7230
7231 #[tokio::test]
7232 async fn grpc_unary_rejects_second_message_before_decompression() {
7233 use buffa_types::google::protobuf::__buffa::view::StringValueView;
7234
7235 let mut body = BytesMut::new();
7236 body.extend_from_slice(&Envelope::data(Bytes::new()).encode());
7237 body.extend_from_slice(&Envelope::compressed(Bytes::from_static(b"not-gzip")).encode());
7238
7239 let response = Response::builder()
7240 .header(http::header::CONTENT_TYPE, "application/grpc+proto")
7241 .body(Full::new(body.freeze()))
7242 .unwrap();
7243 let config =
7244 ClientConfig::new("http://localhost".parse().unwrap()).with_protocol(Protocol::Grpc);
7245
7246 let err = parse_grpc_unary_response::<_, StringValueView<'static>>(
7247 response,
7248 &config,
7249 &CallOptions::default(),
7250 None,
7251 )
7252 .await
7253 .unwrap_err();
7254 assert_eq!(err.code, ErrorCode::Unimplemented);
7255 assert_eq!(
7256 err.message.as_deref(),
7257 Some("received multiple response messages where exactly one was expected")
7258 );
7259 }
7260
7261 #[tokio::test]
7262 async fn grpc_web_unary_stops_reading_after_trailer_frame() {
7263 use buffa_types::google::protobuf::__buffa::view::StringValueView;
7264
7265 let mut body = BytesMut::new();
7266 body.extend_from_slice(&Envelope::data(Bytes::from_static(b"\x0a\x02hi")).encode());
7267 let trailer_payload = b"grpc-status: 0\r\n";
7268 body.extend_from_slice(&[0x80]);
7269 body.extend_from_slice(&(trailer_payload.len() as u32).to_be_bytes());
7270 body.extend_from_slice(trailer_payload);
7271
7272 let (tx, rx) = tokio::sync::mpsc::channel(2);
7273 tx.send(Ok(body.freeze())).await.unwrap();
7274 tx.send(Ok(Bytes::from_static(b"server is still writing")))
7275 .await
7276 .unwrap();
7277
7278 let response = Response::builder()
7281 .header(http::header::CONTENT_TYPE, "application/grpc-web+proto")
7282 .body(ChannelBody { rx })
7283 .unwrap();
7284 let config =
7285 ClientConfig::new("http://localhost".parse().unwrap()).with_protocol(Protocol::GrpcWeb);
7286
7287 let response = tokio::time::timeout(
7288 Duration::from_secs(1),
7289 parse_grpc_unary_response::<_, StringValueView<'static>>(
7290 response,
7291 &config,
7292 &CallOptions::default(),
7293 None,
7294 ),
7295 )
7296 .await
7297 .expect("parser should stop after the gRPC-Web trailer frame")
7298 .unwrap();
7299 assert_eq!(
7300 response.trailers().get("grpc-status").unwrap(),
7301 http::HeaderValue::from_static("0")
7302 );
7303 }
7304
7305 #[tokio::test]
7306 async fn grpc_unary_accepts_bare_application_grpc_with_parameters() {
7307 use buffa::Message;
7308 use buffa_types::google::protobuf::__buffa::view::StringValueView;
7309 use buffa_types::google::protobuf::StringValue;
7310 use http_body::Frame;
7311 use http_body_util::StreamBody;
7312
7313 let data = Envelope::data(StringValue::from("hi").encode_to_bytes()).encode();
7314 let mut trailers = http::HeaderMap::new();
7315 trailers.insert("grpc-status", "0".parse().unwrap());
7316 let frames: Vec<Result<Frame<Bytes>, std::convert::Infallible>> =
7317 vec![Ok(Frame::data(data)), Ok(Frame::trailers(trailers))];
7318
7319 let response = Response::builder()
7320 .header(
7321 http::header::CONTENT_TYPE,
7322 "application/grpc; charset=utf-8",
7323 )
7324 .body(StreamBody::new(futures::stream::iter(frames)))
7325 .unwrap();
7326 let config =
7327 ClientConfig::new("http://localhost".parse().unwrap()).with_protocol(Protocol::Grpc);
7328
7329 let response = parse_grpc_unary_response::<_, StringValueView<'static>>(
7330 response,
7331 &config,
7332 &CallOptions::default(),
7333 None,
7334 )
7335 .await
7336 .expect("bare application/grpc must be accepted as proto");
7337 assert_eq!(response.view().value, "hi");
7338 assert_eq!(response.trailers().get("grpc-status").unwrap(), "0");
7339 }
7340
7341 #[tokio::test]
7342 async fn grpc_unary_rejects_grpc_web_content_type() {
7343 use buffa_types::google::protobuf::__buffa::view::StringValueView;
7344
7345 let response = Response::builder()
7346 .header(http::header::CONTENT_TYPE, "application/grpc-web+proto")
7347 .body(Full::new(Bytes::new()))
7348 .unwrap();
7349 let config =
7350 ClientConfig::new("http://localhost".parse().unwrap()).with_protocol(Protocol::Grpc);
7351
7352 let err = parse_grpc_unary_response::<_, StringValueView<'static>>(
7353 response,
7354 &config,
7355 &CallOptions::default(),
7356 None,
7357 )
7358 .await
7359 .expect_err("gRPC client must reject gRPC-Web content type");
7360 assert_eq!(err.code, ErrorCode::Unknown);
7364 assert_eq!(
7365 err.message.as_deref(),
7366 Some(
7367 "unexpected content-type: application/grpc-web+proto (expected application/grpc+proto)"
7368 )
7369 );
7370 }
7371
7372 #[tokio::test]
7373 async fn grpc_unary_rejects_mismatched_codec_content_type() {
7374 use buffa_types::google::protobuf::__buffa::view::StringValueView;
7375
7376 let response = Response::builder()
7377 .header(http::header::CONTENT_TYPE, "application/grpc+json")
7378 .body(Full::new(Bytes::new()))
7379 .unwrap();
7380 let config =
7381 ClientConfig::new("http://localhost".parse().unwrap()).with_protocol(Protocol::Grpc);
7382
7383 let err = parse_grpc_unary_response::<_, StringValueView<'static>>(
7384 response,
7385 &config,
7386 &CallOptions::default(),
7387 None,
7388 )
7389 .await
7390 .expect_err("gRPC client must reject mismatched response codec");
7391 assert_eq!(err.code, ErrorCode::Internal);
7392 assert_eq!(
7393 err.message.as_deref(),
7394 Some(
7395 "unexpected content-type: application/grpc+json (expected application/grpc+proto)"
7396 )
7397 );
7398 }
7399
7400 #[test]
7401 fn grpc_response_content_type_rejects_non_grpc_as_unknown() {
7402 let mut headers = http::HeaderMap::new();
7403 headers.insert(http::header::CONTENT_TYPE, "text/html".parse().unwrap());
7404 let config =
7405 ClientConfig::new("http://localhost".parse().unwrap()).with_protocol(Protocol::Grpc);
7406
7407 let err = validate_grpc_response_content_type(&headers, &config)
7408 .expect_err("non-gRPC content type must be rejected");
7409 assert_eq!(err.code, ErrorCode::Unknown);
7410 assert_eq!(
7411 err.message.as_deref(),
7412 Some("unexpected content-type: text/html (expected application/grpc+proto)")
7413 );
7414 assert!(
7415 err.response_headers()
7416 .contains_key(http::header::CONTENT_TYPE)
7417 );
7418 }
7419
7420 #[test]
7421 fn grpc_response_content_type_accepts_missing_header() {
7422 let config =
7423 ClientConfig::new("http://localhost".parse().unwrap()).with_protocol(Protocol::Grpc);
7424 validate_grpc_response_content_type(&http::HeaderMap::new(), &config)
7425 .expect("missing content-type must remain accepted");
7426 }
7427
7428 #[test]
7429 fn grpc_response_content_type_accepts_bare_for_json_codec() {
7430 let mut headers = http::HeaderMap::new();
7434 headers.insert(
7435 http::header::CONTENT_TYPE,
7436 "application/grpc".parse().unwrap(),
7437 );
7438 let config = ClientConfig::new("http://localhost".parse().unwrap())
7439 .with_protocol(Protocol::Grpc)
7440 .with_codec_format(CodecFormat::Json);
7441 validate_grpc_response_content_type(&headers, &config)
7442 .expect("bare application/grpc must be accepted for a json-codec client");
7443 }
7444
7445 #[test]
7446 fn grpc_web_response_content_type_accepts_bare() {
7447 let mut headers = http::HeaderMap::new();
7448 headers.insert(
7449 http::header::CONTENT_TYPE,
7450 "application/grpc-web".parse().unwrap(),
7451 );
7452 let config =
7453 ClientConfig::new("http://localhost".parse().unwrap()).with_protocol(Protocol::GrpcWeb);
7454 validate_grpc_response_content_type(&headers, &config)
7455 .expect("bare application/grpc-web must be accepted as proto");
7456 }
7457
7458 #[test]
7459 fn grpc_web_response_content_type_rejects_grpc_as_unknown() {
7460 let mut headers = http::HeaderMap::new();
7461 headers.insert(
7462 http::header::CONTENT_TYPE,
7463 "application/grpc+proto".parse().unwrap(),
7464 );
7465 let config =
7466 ClientConfig::new("http://localhost".parse().unwrap()).with_protocol(Protocol::GrpcWeb);
7467
7468 let err = validate_grpc_response_content_type(&headers, &config)
7469 .expect_err("gRPC-Web client must reject plain gRPC content type");
7470 assert_eq!(err.code, ErrorCode::Unknown);
7471 assert_eq!(
7472 err.message.as_deref(),
7473 Some(
7474 "unexpected content-type: application/grpc+proto (expected application/grpc-web+proto)"
7475 )
7476 );
7477 }
7478
7479 #[test]
7484 fn test_unary_request_content_type_connect() {
7485 let config = ClientConfig::new("http://localhost".parse().unwrap());
7486 assert_eq!(unary_request_content_type(&config), "application/proto");
7487
7488 let config = config.with_codec_format(CodecFormat::Json);
7489 assert_eq!(unary_request_content_type(&config), "application/json");
7490 }
7491
7492 #[cfg(not(feature = "json"))]
7493 #[test]
7494 fn decode_response_view_json_is_unimplemented_without_feature() {
7495 use buffa::Message;
7496 use buffa_types::google::protobuf::__buffa::view::StringValueView;
7497 use buffa_types::google::protobuf::StringValue;
7498 let err = decode_response_view::<StringValueView>(
7503 Bytes::from_static(b"\"x\""),
7504 CodecFormat::Json,
7505 )
7506 .unwrap_err();
7507 assert_eq!(err.code, ErrorCode::Unimplemented);
7508
7509 let bytes = StringValue::from("ok").encode_to_bytes();
7511 assert!(decode_response_view::<StringValueView>(bytes, CodecFormat::Proto).is_ok());
7512 }
7513
7514 #[test]
7515 fn test_unary_request_content_type_grpc() {
7516 let config =
7517 ClientConfig::new("http://localhost".parse().unwrap()).with_protocol(Protocol::Grpc);
7518 assert_eq!(
7519 unary_request_content_type(&config),
7520 "application/grpc+proto"
7521 );
7522
7523 let config = config.with_codec_format(CodecFormat::Json);
7524 assert_eq!(unary_request_content_type(&config), "application/grpc+json");
7525 }
7526
7527 #[test]
7528 fn test_streaming_request_content_type() {
7529 let config = ClientConfig::new("http://localhost".parse().unwrap());
7530 assert_eq!(
7531 streaming_request_content_type(&config),
7532 "application/connect+proto"
7533 );
7534
7535 let config = config.with_protocol(Protocol::Grpc);
7536 assert_eq!(
7537 streaming_request_content_type(&config),
7538 "application/grpc+proto"
7539 );
7540
7541 let config = config.with_protocol(Protocol::GrpcWeb);
7542 assert_eq!(
7543 streaming_request_content_type(&config),
7544 "application/grpc-web+proto"
7545 );
7546 }
7547
7548 #[test]
7553 fn test_http_status_to_error_code() {
7554 assert_eq!(
7555 http_status_to_error_code(http::StatusCode::BAD_REQUEST),
7556 ErrorCode::Internal
7557 );
7558 assert_eq!(
7559 http_status_to_error_code(http::StatusCode::UNAUTHORIZED),
7560 ErrorCode::Unauthenticated
7561 );
7562 assert_eq!(
7563 http_status_to_error_code(http::StatusCode::FORBIDDEN),
7564 ErrorCode::PermissionDenied
7565 );
7566 assert_eq!(
7567 http_status_to_error_code(http::StatusCode::NOT_FOUND),
7568 ErrorCode::Unimplemented
7569 );
7570 assert_eq!(
7571 http_status_to_error_code(http::StatusCode::SERVICE_UNAVAILABLE),
7572 ErrorCode::Unavailable
7573 );
7574 assert_eq!(
7575 http_status_to_error_code(http::StatusCode::INTERNAL_SERVER_ERROR),
7576 ErrorCode::Unknown
7577 );
7578 }
7579
7580 #[test]
7585 fn test_add_unary_request_headers_connect() {
7586 let config = ClientConfig::new("http://localhost".parse().unwrap());
7587 let builder = http::Request::builder();
7588 let builder = add_unary_request_headers(builder, &config, None, None);
7589 let req = builder.body(()).unwrap();
7590 assert_eq!(
7591 req.headers().get("content-type").unwrap(),
7592 "application/proto"
7593 );
7594 assert_eq!(req.headers().get("connect-protocol-version").unwrap(), "1");
7595 assert!(req.headers().get("te").is_none());
7596 }
7597
7598 #[test]
7599 fn test_add_unary_request_headers_grpc() {
7600 let config =
7601 ClientConfig::new("http://localhost".parse().unwrap()).with_protocol(Protocol::Grpc);
7602 let builder = http::Request::builder();
7603 let builder = add_unary_request_headers(builder, &config, None, None);
7604 let req = builder.body(()).unwrap();
7605 assert_eq!(
7606 req.headers().get("content-type").unwrap(),
7607 "application/grpc+proto"
7608 );
7609 assert_eq!(req.headers().get("te").unwrap(), "trailers");
7610 assert!(req.headers().get("connect-protocol-version").is_none());
7611 }
7612
7613 #[test]
7614 fn test_add_unary_request_headers_grpc_web() {
7615 let config =
7616 ClientConfig::new("http://localhost".parse().unwrap()).with_protocol(Protocol::GrpcWeb);
7617 let builder = http::Request::builder();
7618 let builder = add_unary_request_headers(builder, &config, None, None);
7619 let req = builder.body(()).unwrap();
7620 assert_eq!(
7621 req.headers().get("content-type").unwrap(),
7622 "application/grpc-web+proto"
7623 );
7624 assert!(req.headers().get("te").is_none());
7625 assert!(req.headers().get("connect-protocol-version").is_none());
7626 }
7627
7628 #[test]
7629 fn test_add_unary_request_headers_with_timeout() {
7630 let config =
7631 ClientConfig::new("http://localhost".parse().unwrap()).with_protocol(Protocol::Grpc);
7632 let builder = http::Request::builder();
7633 let builder =
7634 add_unary_request_headers(builder, &config, Some(Duration::from_millis(500)), None);
7635 let req = builder.body(()).unwrap();
7636 assert_eq!(req.headers().get("grpc-timeout").unwrap(), "500m");
7637 }
7638
7639 #[tokio::test]
7644 async fn with_deadline_none_passes_through() {
7645 let result: Result<i32, ConnectError> = with_deadline(None, async { Ok(42) }).await;
7646 assert_eq!(result.unwrap(), 42);
7647 }
7648
7649 #[tokio::test]
7650 async fn with_deadline_completes_before_deadline() {
7651 let deadline = std::time::Instant::now() + Duration::from_secs(10);
7652 let result: Result<i32, ConnectError> =
7653 with_deadline(Some(deadline), async { Ok(42) }).await;
7654 assert_eq!(result.unwrap(), 42);
7655 }
7656
7657 #[tokio::test(start_paused = true)]
7658 async fn with_deadline_fires_on_slow_future() {
7659 let deadline = std::time::Instant::now() + Duration::from_millis(100);
7660 let slow = async {
7661 tokio::time::sleep(Duration::from_secs(10)).await;
7662 Ok::<i32, ConnectError>(42)
7663 };
7664 let result = with_deadline(Some(deadline), slow).await;
7665 let err = result.unwrap_err();
7666 assert_eq!(err.code, ErrorCode::DeadlineExceeded);
7667 }
7668
7669 #[tokio::test(start_paused = true)]
7670 async fn with_deadline_already_passed_returns_immediately() {
7671 let deadline = std::time::Instant::now() - Duration::from_secs(1);
7674 let result: Result<i32, ConnectError> =
7675 with_deadline(Some(deadline), std::future::pending()).await;
7676 let err = result.unwrap_err();
7677 assert_eq!(err.code, ErrorCode::DeadlineExceeded);
7678 }
7679
7680 #[tokio::test]
7681 async fn with_deadline_propagates_inner_error() {
7682 let deadline = std::time::Instant::now() + Duration::from_secs(10);
7683 let failing = async { Err::<i32, _>(ConnectError::internal("inner")) };
7684 let result = with_deadline(Some(deadline), failing).await;
7685 let err = result.unwrap_err();
7686 assert_eq!(err.code, ErrorCode::Internal);
7687 }
7688
7689 #[tokio::test]
7694 async fn channel_body_delivers_frames_then_eof() {
7695 let (tx, rx) = tokio::sync::mpsc::channel(4);
7696 let body = ChannelBody { rx };
7697
7698 tx.send(Ok(Bytes::from_static(b"hello"))).await.unwrap();
7699 tx.send(Ok(Bytes::from_static(b"world"))).await.unwrap();
7700 drop(tx); let collected = body.collect().await.unwrap().to_bytes();
7703 assert_eq!(&collected[..], b"helloworld");
7704 }
7705
7706 #[tokio::test]
7707 async fn channel_body_surfaces_error() {
7708 let (tx, rx) = tokio::sync::mpsc::channel(4);
7709 let mut body = ChannelBody { rx };
7710
7711 tx.send(Err(ConnectError::internal("boom"))).await.unwrap();
7712 drop(tx);
7713
7714 let frame = std::future::poll_fn(|cx| Pin::new(&mut body).poll_frame(cx)).await;
7715 assert!(matches!(frame, Some(Err(_))));
7716 }
7717
7718 #[tokio::test]
7723 async fn collect_body_bounded_within_limit() {
7724 let body = Full::new(Bytes::from_static(b"hello"));
7725 let got = collect_body_bounded(body, 10, None).await.unwrap();
7726 assert_eq!(&got[..], b"hello");
7727 }
7728
7729 #[tokio::test]
7730 async fn collect_body_bounded_at_exact_limit() {
7731 let body = Full::new(Bytes::from_static(b"hello"));
7732 let got = collect_body_bounded(body, 5, None).await.unwrap();
7733 assert_eq!(&got[..], b"hello");
7734 }
7735
7736 #[tokio::test]
7737 async fn collect_body_bounded_exceeds_limit() {
7738 let body = Full::new(Bytes::from_static(b"hello world"));
7739 let err = collect_body_bounded(body, 5, None).await.unwrap_err();
7740 assert_eq!(err.code, ErrorCode::ResourceExhausted);
7741 }
7742
7743 #[tokio::test]
7744 async fn collect_body_bounded_empty() {
7745 let body = Full::new(Bytes::new());
7746 let got = collect_body_bounded(body, 0, None).await.unwrap();
7747 assert!(got.is_empty());
7748 }
7749
7750 #[tokio::test]
7751 async fn collect_body_bounded_multi_frame_exceeds_mid_stream() {
7752 let (tx, rx) = tokio::sync::mpsc::channel(4);
7753 let body = ChannelBody { rx };
7754 tx.send(Ok(Bytes::from_static(b"aaa"))).await.unwrap();
7755 tx.send(Ok(Bytes::from_static(b"bbb"))).await.unwrap();
7756 tx.send(Ok(Bytes::from_static(b"ccc"))).await.unwrap();
7757 drop(tx);
7758 let err = collect_body_bounded(body, 7, None).await.unwrap_err();
7760 assert_eq!(err.code, ErrorCode::ResourceExhausted);
7761 }
7762
7763 #[tokio::test]
7764 async fn collect_body_bounded_multi_frame_within_limit() {
7765 let (tx, rx) = tokio::sync::mpsc::channel(4);
7766 let body = ChannelBody { rx };
7767 tx.send(Ok(Bytes::from_static(b"foo"))).await.unwrap();
7768 tx.send(Ok(Bytes::from_static(b"bar"))).await.unwrap();
7769 drop(tx);
7770 let got = collect_body_bounded(body, 10, None).await.unwrap();
7771 assert_eq!(&got[..], b"foobar");
7772 }
7773
7774 #[tokio::test]
7775 async fn collect_body_bounded_propagates_body_error() {
7776 let (tx, rx) = tokio::sync::mpsc::channel(4);
7777 let body = ChannelBody { rx };
7778 tx.send(Err(ConnectError::internal("io"))).await.unwrap();
7779 drop(tx);
7780 let err = collect_body_bounded(body, 1024, None).await.unwrap_err();
7781 assert_eq!(err.code, ErrorCode::Internal);
7782 }
7783
7784 #[tokio::test]
7789 async fn a_body_error_after_the_deadline_is_deadline_exceeded() {
7790 let (tx, rx) = tokio::sync::mpsc::channel(4);
7791 let body = ChannelBody { rx };
7792 tx.send(Err(ConnectError::internal("io"))).await.unwrap();
7793 drop(tx);
7794
7795 let elapsed = std::time::Instant::now() - Duration::from_millis(1);
7796 let err = collect_body_bounded(body, 1024, Some(elapsed))
7797 .await
7798 .unwrap_err();
7799 assert_eq!(err.code, ErrorCode::DeadlineExceeded);
7800 assert!(
7804 err.message.as_deref().unwrap_or_default().contains("io"),
7805 "got {:?}",
7806 err.message
7807 );
7808 }
7809
7810 #[tokio::test]
7814 async fn a_body_error_before_the_deadline_stays_internal() {
7815 let (tx, rx) = tokio::sync::mpsc::channel(4);
7816 let body = ChannelBody { rx };
7817 tx.send(Err(ConnectError::internal("io"))).await.unwrap();
7818 drop(tx);
7819
7820 let future = std::time::Instant::now() + Duration::from_secs(60);
7821 let err = collect_body_bounded(body, 1024, Some(future))
7822 .await
7823 .unwrap_err();
7824 assert_eq!(err.code, ErrorCode::Internal);
7825 }
7826
7827 #[tokio::test]
7831 async fn a_body_error_without_a_deadline_is_not_described_as_a_timeout() {
7832 let (tx, rx) = tokio::sync::mpsc::channel(4);
7833 let body = ChannelBody { rx };
7834 tx.send(Err(ConnectError::internal("io"))).await.unwrap();
7835 drop(tx);
7836
7837 let err = collect_body_bounded(body, 1024, None).await.unwrap_err();
7838 assert_eq!(err.code, ErrorCode::Internal);
7839 assert!(
7840 !err.message
7841 .as_deref()
7842 .unwrap_or_default()
7843 .contains("deadline"),
7844 "got {:?}",
7845 err.message
7846 );
7847 }
7848
7849 #[test]
7850 fn test_add_streaming_request_headers_grpc() {
7851 let config =
7852 ClientConfig::new("http://localhost".parse().unwrap()).with_protocol(Protocol::Grpc);
7853 let builder = http::Request::builder();
7854 let builder = add_streaming_request_headers(builder, &config, None);
7855 let req = builder.body(()).unwrap();
7856 assert_eq!(
7857 req.headers().get("content-type").unwrap(),
7858 "application/grpc+proto"
7859 );
7860 assert_eq!(req.headers().get("te").unwrap(), "trailers");
7861 }
7862
7863 #[test]
7868 fn test_client_config_protocol() {
7869 let config =
7870 ClientConfig::new("http://localhost".parse().unwrap()).with_protocol(Protocol::Grpc);
7871 assert_eq!(config.protocol, Protocol::Grpc);
7872 }
7873
7874 #[test]
7875 fn test_client_config_default_protocol() {
7876 let config = ClientConfig::new("http://localhost".parse().unwrap());
7877 assert_eq!(config.protocol, Protocol::Connect);
7878 }
7879
7880 fn headers_for(protocol: Protocol, applied_encoding: Option<&str>) -> http::HeaderMap {
7885 let config = ClientConfig::new("http://localhost".parse().unwrap())
7886 .with_protocol(protocol)
7887 .compress_requests("gzip");
7888 let builder = http::Request::builder();
7889 add_unary_request_headers(builder, &config, None, applied_encoding)
7890 .body(())
7891 .unwrap()
7892 .headers()
7893 .clone()
7894 }
7895
7896 #[test]
7897 fn connect_unary_no_content_encoding_when_compression_skipped() {
7898 let headers = headers_for(Protocol::Connect, None);
7900 assert!(
7901 !headers.contains_key(http::header::CONTENT_ENCODING),
7902 "Content-Encoding must not be set when compression policy skipped the body"
7903 );
7904 }
7905
7906 #[test]
7907 fn connect_unary_content_encoding_when_compressed() {
7908 let headers = headers_for(Protocol::Connect, Some("gzip"));
7909 assert_eq!(headers.get(http::header::CONTENT_ENCODING).unwrap(), "gzip");
7910 }
7911
7912 #[test]
7913 fn grpc_unary_encoding_header_independent_of_applied() {
7914 let headers = headers_for(Protocol::Grpc, None);
7918 assert_eq!(headers.get("grpc-encoding").unwrap(), "gzip");
7919 }
7920
7921 fn test_config() -> ClientConfig {
7926 ClientConfig::new("http://localhost:8080".parse().unwrap())
7927 }
7928
7929 #[test]
7930 fn effective_options_uses_config_defaults_when_options_unset() {
7931 let config = test_config()
7932 .with_default_timeout(Duration::from_secs(30))
7933 .with_default_max_message_size(1024)
7934 .with_default_header("x-trace-id", "cfg-trace");
7935
7936 let eff = effective_options(&config, CallOptions::default());
7937
7938 assert_eq!(eff.timeout, Some(Duration::from_secs(30)));
7939 assert_eq!(eff.max_message_size, Some(1024));
7940 assert_eq!(eff.headers.get("x-trace-id").unwrap(), "cfg-trace");
7941 }
7942
7943 #[test]
7944 fn effective_options_options_override_config_defaults() {
7945 let config = test_config()
7946 .with_default_timeout(Duration::from_secs(30))
7947 .with_default_max_message_size(1024);
7948
7949 let options = CallOptions::default()
7950 .with_timeout(Duration::from_secs(5))
7951 .with_max_message_size(512);
7952
7953 let eff = effective_options(&config, options);
7954
7955 assert_eq!(eff.timeout, Some(Duration::from_secs(5)));
7956 assert_eq!(eff.max_message_size, Some(512));
7957 }
7958
7959 #[test]
7960 fn effective_options_compress_has_no_config_default() {
7961 let config = test_config();
7962 let options = CallOptions::default().with_compress(true);
7963 let eff = effective_options(&config, options);
7964 assert_eq!(eff.compress, Some(true));
7965 }
7966
7967 #[test]
7968 fn merge_headers_options_override_config_same_name() {
7969 let mut cfg = http::HeaderMap::new();
7970 cfg.insert("x-token", "cfg-token".parse().unwrap());
7971
7972 let mut opts = http::HeaderMap::new();
7973 opts.insert("x-token", "opt-token".parse().unwrap());
7974
7975 let merged = merge_headers(&cfg, opts);
7976 let vals: Vec<_> = merged.get_all("x-token").iter().collect();
7977 assert_eq!(vals.len(), 1);
7978 assert_eq!(vals[0], "opt-token");
7979 }
7980
7981 #[test]
7982 fn merge_headers_config_only_names_preserved() {
7983 let mut cfg = http::HeaderMap::new();
7984 cfg.insert("x-cfg-only", "kept".parse().unwrap());
7985
7986 let mut opts = http::HeaderMap::new();
7987 opts.insert("x-opt-only", "also-kept".parse().unwrap());
7988
7989 let merged = merge_headers(&cfg, opts);
7990 assert_eq!(merged.get("x-cfg-only").unwrap(), "kept");
7991 assert_eq!(merged.get("x-opt-only").unwrap(), "also-kept");
7992 }
7993
7994 #[test]
7995 fn merge_headers_options_multivalue_replaces_config() {
7996 let mut cfg = http::HeaderMap::new();
7997 cfg.append("x-thing", "cfg-a".parse().unwrap());
7998 cfg.append("x-thing", "cfg-b".parse().unwrap());
7999
8000 let mut opts = http::HeaderMap::new();
8001 opts.append("x-thing", "opt-1".parse().unwrap());
8002 opts.append("x-thing", "opt-2".parse().unwrap());
8003
8004 let merged = merge_headers(&cfg, opts);
8005 let vals: Vec<_> = merged
8006 .get_all("x-thing")
8007 .iter()
8008 .map(|v| v.to_str().unwrap())
8009 .collect();
8010 assert_eq!(vals, vec!["opt-1", "opt-2"]);
8011 }
8012
8013 #[test]
8014 fn merge_headers_empty_config_fast_path() {
8015 let cfg = http::HeaderMap::new();
8016 let mut opts = http::HeaderMap::new();
8017 opts.insert("x", "y".parse().unwrap());
8018
8019 let merged = merge_headers(&cfg, opts);
8020 assert_eq!(merged.get("x").unwrap(), "y");
8021 }
8022
8023 #[test]
8024 fn merge_headers_empty_options_fast_path() {
8025 let mut cfg = http::HeaderMap::new();
8026 cfg.insert("x", "y".parse().unwrap());
8027 let opts = http::HeaderMap::new();
8028
8029 let merged = merge_headers(&cfg, opts);
8030 assert_eq!(merged.get("x").unwrap(), "y");
8031 }
8032
8033 fn assert_connect_get_param_order(query: &str) {
8042 const RANK: &[&str] = &["connect", "base64", "compression", "encoding", "message"];
8043 let mut last = 0;
8044 for pair in query.split('&') {
8045 let key = pair.split_once('=').map_or(pair, |(k, _)| k);
8046 let rank = RANK
8047 .iter()
8048 .position(|k| *k == key)
8049 .unwrap_or_else(|| panic!("unknown query parameter {key:?} in {query:?}"));
8050 assert!(
8051 rank >= last,
8052 "parameter {key:?} out of recommended order in {query:?}",
8053 );
8054 last = rank;
8055 }
8056 }
8057
8058 #[test]
8059 fn get_query_param_order_proto() {
8060 let q = build_connect_get_query(true, None, "proto", "AAAA");
8061 assert_eq!(q, "connect=v1&base64=1&encoding=proto&message=AAAA");
8062 assert_connect_get_param_order(&q);
8063 }
8064
8065 #[test]
8066 fn get_query_param_order_json_uncompressed() {
8067 let q = build_connect_get_query(false, None, "json", "%7B%7D");
8068 assert_eq!(q, "connect=v1&encoding=json&message=%7B%7D");
8069 assert_connect_get_param_order(&q);
8070 }
8071
8072 #[test]
8073 fn get_query_param_order_compressed() {
8074 let q = build_connect_get_query(true, Some("gzip"), "proto", "H4sI");
8075 assert_eq!(
8076 q,
8077 "connect=v1&base64=1&compression=gzip&encoding=proto&message=H4sI",
8078 );
8079 assert_connect_get_param_order(&q);
8080 }
8081
8082 #[test]
8083 fn get_query_param_order_json_compressed() {
8084 let q = build_connect_get_query(true, Some("gzip"), "json", "H4sI");
8086 assert_eq!(
8087 q,
8088 "connect=v1&base64=1&compression=gzip&encoding=json&message=H4sI",
8089 );
8090 assert_connect_get_param_order(&q);
8091 }
8092
8093 #[test]
8094 fn get_base64_encoding_matches_rfc4648_urlsafe_no_pad() {
8095 use base64::Engine;
8099 let encoded = base64::engine::general_purpose::URL_SAFE_NO_PAD.encode(b"\xfa\xfb\xfc");
8100 assert!(!encoded.contains('+'), "URL-safe must not contain +");
8105 assert!(!encoded.contains('/'), "URL-safe must not contain /");
8106 assert!(!encoded.contains('='), "no-pad must not contain =");
8107
8108 let decoded = base64::engine::general_purpose::URL_SAFE_NO_PAD
8110 .decode(&encoded)
8111 .unwrap();
8112 assert_eq!(decoded, b"\xfa\xfb\xfc");
8113 }
8114
8115 #[cfg(feature = "gzip")]
8124 #[test]
8125 fn client_stream_response_rejects_second_message_before_decompression() {
8126 let registry = crate::compression::CompressionRegistry::default();
8127
8128 let mut body = Envelope::data(Bytes::from_static(b"first"))
8129 .encode()
8130 .to_vec();
8131 body.extend_from_slice(
8132 &Envelope::compressed(Bytes::from_static(b"not gzip data")).encode(),
8133 );
8134 body.extend_from_slice(&Envelope::end_stream(Bytes::from_static(b"{}")).encode());
8135
8136 let err = parse_connect_client_stream_envelopes(
8137 Bytes::from(body),
8138 ®istry,
8139 Some("gzip"),
8140 1024 * 1024,
8141 &http::HeaderMap::new(),
8142 )
8143 .unwrap_err();
8144 assert_eq!(err.code, ErrorCode::Unimplemented);
8145 assert!(
8146 err.to_string().contains("multiple data messages"),
8147 "unexpected error: {err}"
8148 );
8149 }
8150
8151 #[cfg(feature = "gzip")]
8157 #[test]
8158 fn malformed_compressed_response_payload_is_data_loss() {
8159 let registry = crate::compression::CompressionRegistry::default();
8160
8161 let mut body = Envelope::compressed(Bytes::from_static(b"not gzip data"))
8162 .encode()
8163 .to_vec();
8164 body.extend_from_slice(&Envelope::end_stream(Bytes::from_static(b"{}")).encode());
8165
8166 let err = parse_connect_client_stream_envelopes(
8167 Bytes::from(body),
8168 ®istry,
8169 Some("gzip"),
8170 1024 * 1024,
8171 &http::HeaderMap::new(),
8172 )
8173 .unwrap_err();
8174 assert_eq!(err.code, ErrorCode::DataLoss, "unexpected error: {err}");
8175 }
8176
8177 #[cfg(feature = "gzip")]
8181 #[test]
8182 fn client_stream_end_stream_decompression_failure_carries_headers() {
8183 use crate::envelope::flags;
8184
8185 let registry = crate::compression::CompressionRegistry::default();
8186
8187 let mut body = Envelope::data(Bytes::from_static(b"only"))
8188 .encode()
8189 .to_vec();
8190 body.extend_from_slice(
8191 &Envelope {
8192 flags: flags::COMPRESSED | flags::END_STREAM,
8193 data: Bytes::from_static(b"not gzip data"),
8194 }
8195 .encode(),
8196 );
8197
8198 let mut resp_headers = http::HeaderMap::new();
8199 resp_headers.insert("x-from-headers", http::HeaderValue::from_static("yes"));
8200
8201 let err = parse_connect_client_stream_envelopes(
8202 Bytes::from(body),
8203 ®istry,
8204 Some("gzip"),
8205 1024 * 1024,
8206 &resp_headers,
8207 )
8208 .unwrap_err();
8209 assert_eq!(err.code, ErrorCode::DataLoss, "unexpected error: {err}");
8210 assert_eq!(err.response_headers().get("x-from-headers").unwrap(), "yes");
8211 }
8212
8213 #[test]
8217 fn response_decompression_error_remap() {
8218 let e = map_response_decompression_error(ConnectError::invalid_argument("corrupt"));
8219 assert_eq!(e.code, ErrorCode::DataLoss);
8220 let e = map_response_decompression_error(ConnectError::unimplemented("unknown encoding"));
8221 assert_eq!(e.code, ErrorCode::Internal);
8222 let e = map_response_decompression_error(ConnectError::resource_exhausted("too big"));
8223 assert_eq!(e.code, ErrorCode::ResourceExhausted);
8224 }
8225
8226 #[test]
8230 fn client_stream_response_stops_at_end_stream() {
8231 let registry = crate::compression::CompressionRegistry::new();
8232
8233 let mut body = Envelope::data(Bytes::from_static(b"only"))
8234 .encode()
8235 .to_vec();
8236 body.extend_from_slice(
8237 &Envelope::end_stream(Bytes::from_static(b"{\"metadata\":{\"x-extra\":[\"1\"]}}"))
8238 .encode(),
8239 );
8240 body.extend_from_slice(&[0xAA_u8; 256]);
8241
8242 let (message, trailers) = parse_connect_client_stream_envelopes(
8243 Bytes::from(body),
8244 ®istry,
8245 None,
8246 1024,
8247 &http::HeaderMap::new(),
8248 )
8249 .unwrap();
8250 assert_eq!(&message[..], b"only");
8251 assert_eq!(trailers.get("x-extra").unwrap(), "1");
8252 }
8253
8254 #[test]
8257 fn client_stream_response_end_stream_error() {
8258 let registry = crate::compression::CompressionRegistry::new();
8259
8260 let mut body = Envelope::data(Bytes::from_static(b"only"))
8261 .encode()
8262 .to_vec();
8263 body.extend_from_slice(
8264 &Envelope::end_stream(Bytes::from_static(
8265 b"{\"metadata\":{\"x-meta\":[\"m\"]},\"error\":{\"code\":\"resource_exhausted\",\"message\":\"too much\"}}",
8266 ))
8267 .encode(),
8268 );
8269
8270 let mut resp_headers = http::HeaderMap::new();
8271 resp_headers.insert("x-from-headers", http::HeaderValue::from_static("yes"));
8272
8273 let err = parse_connect_client_stream_envelopes(
8274 Bytes::from(body),
8275 ®istry,
8276 None,
8277 1024,
8278 &resp_headers,
8279 )
8280 .unwrap_err();
8281 assert_eq!(err.code, ErrorCode::ResourceExhausted);
8282 assert_eq!(err.message.as_deref(), Some("too much"));
8283 assert_eq!(
8284 err.response_headers().get("x-from-headers").unwrap(),
8285 "yes",
8286 "response headers must be attached to the END_STREAM error"
8287 );
8288 assert_eq!(
8289 err.trailers().get("x-meta").unwrap(),
8290 "m",
8291 "END_STREAM metadata must be attached to the error as trailers"
8292 );
8293 }
8294
8295 #[test]
8298 fn client_stream_response_malformed_end_stream_json_errors() {
8299 let registry = crate::compression::CompressionRegistry::new();
8300
8301 let mut body = Envelope::data(Bytes::from_static(b"only"))
8302 .encode()
8303 .to_vec();
8304 body.extend_from_slice(&Envelope::end_stream(Bytes::from_static(b"not json")).encode());
8305
8306 let mut resp_headers = http::HeaderMap::new();
8307 resp_headers.insert("x-from-headers", http::HeaderValue::from_static("yes"));
8308
8309 let err = parse_connect_client_stream_envelopes(
8310 Bytes::from(body),
8311 ®istry,
8312 None,
8313 1024,
8314 &resp_headers,
8315 )
8316 .unwrap_err();
8317 assert_eq!(err.code, ErrorCode::Internal);
8318 assert!(
8319 err.to_string()
8320 .contains("malformed Connect END_STREAM JSON"),
8321 "unexpected error: {err}"
8322 );
8323 assert_eq!(err.response_headers().get("x-from-headers").unwrap(), "yes");
8324 assert!(err.trailers().is_empty());
8325 }
8326
8327 #[test]
8329 fn client_stream_response_requires_a_message() {
8330 let registry = crate::compression::CompressionRegistry::new();
8331 let body = Envelope::end_stream(Bytes::from_static(b"{}")).encode();
8332
8333 let err = parse_connect_client_stream_envelopes(
8334 body,
8335 ®istry,
8336 None,
8337 1024,
8338 &http::HeaderMap::new(),
8339 )
8340 .unwrap_err();
8341 assert_eq!(err.code, ErrorCode::Unimplemented);
8342 assert!(
8343 err.to_string().contains("no data messages"),
8344 "unexpected error: {err}"
8345 );
8346 }
8347
8348 #[cfg(feature = "gzip")]
8351 #[test]
8352 fn client_stream_response_compressed_envelopes() {
8353 use crate::compression::{CompressionProvider, GzipProvider};
8354
8355 let registry = crate::compression::CompressionRegistry::default();
8356 let gzip = GzipProvider::default();
8357
8358 let mut body = Envelope::compressed(gzip.compress(b"only").unwrap())
8359 .encode()
8360 .to_vec();
8361 let mut end_stream = Envelope::compressed(
8362 gzip.compress(b"{\"metadata\":{\"x-extra\":[\"1\"]}}")
8363 .unwrap(),
8364 )
8365 .encode()
8366 .to_vec();
8367 end_stream[0] |= 0x02; body.extend_from_slice(&end_stream);
8369
8370 let (message, trailers) = parse_connect_client_stream_envelopes(
8371 Bytes::from(body),
8372 ®istry,
8373 Some("gzip"),
8374 1024 * 1024,
8375 &http::HeaderMap::new(),
8376 )
8377 .unwrap();
8378 assert_eq!(&message[..], b"only");
8379 assert_eq!(trailers.get("x-extra").unwrap(), "1");
8380 }
8381
8382 #[test]
8386 fn client_stream_response_data_after_end_stream_is_not_a_message() {
8387 let registry = crate::compression::CompressionRegistry::new();
8388
8389 let mut body = Envelope::end_stream(Bytes::from_static(b"{}"))
8390 .encode()
8391 .to_vec();
8392 body.extend_from_slice(&Envelope::data(Bytes::from_static(b"late")).encode());
8393
8394 let err = parse_connect_client_stream_envelopes(
8395 Bytes::from(body),
8396 ®istry,
8397 None,
8398 1024,
8399 &http::HeaderMap::new(),
8400 )
8401 .unwrap_err();
8402 assert_eq!(err.code, ErrorCode::Unimplemented);
8403 assert!(
8404 err.to_string().contains("no data messages"),
8405 "unexpected error: {err}"
8406 );
8407 }
8408
8409 #[test]
8414 fn client_stream_response_requires_end_stream_after_message() {
8415 let registry = crate::compression::CompressionRegistry::new();
8416
8417 let body = Envelope::data(Bytes::from_static(b"only")).encode();
8418
8419 let err = parse_connect_client_stream_envelopes(
8420 body,
8421 ®istry,
8422 None,
8423 1024,
8424 &http::HeaderMap::new(),
8425 )
8426 .unwrap_err();
8427 assert_eq!(err.code, ErrorCode::Internal);
8428 assert_eq!(
8429 err.message.as_deref(),
8430 Some("Connect streaming response ended without END_STREAM envelope"),
8431 );
8432 }
8433
8434 #[test]
8439 fn client_stream_response_requires_complete_end_stream_after_message() {
8440 let registry = crate::compression::CompressionRegistry::new();
8441
8442 let mut body = Envelope::data(Bytes::from_static(b"only"))
8443 .encode()
8444 .to_vec();
8445 let end_stream = Envelope::end_stream(Bytes::from_static(b"{}")).encode();
8446 body.extend_from_slice(&end_stream[..end_stream.len() - 1]);
8448
8449 let err = parse_connect_client_stream_envelopes(
8450 Bytes::from(body),
8451 ®istry,
8452 None,
8453 1024,
8454 &http::HeaderMap::new(),
8455 )
8456 .unwrap_err();
8457 assert_eq!(err.code, ErrorCode::Internal);
8458 assert_eq!(
8459 err.message.as_deref(),
8460 Some("Connect streaming response ended without END_STREAM envelope"),
8461 );
8462 }
8463
8464 #[test]
8467 fn client_stream_response_end_stream_completes_the_response() {
8468 let registry = crate::compression::CompressionRegistry::new();
8469
8470 let mut body = Envelope::data(Bytes::from_static(b"only"))
8471 .encode()
8472 .to_vec();
8473 body.extend_from_slice(&Envelope::end_stream(Bytes::from_static(b"{}")).encode());
8474
8475 let (message, trailers) = parse_connect_client_stream_envelopes(
8476 Bytes::from(body),
8477 ®istry,
8478 None,
8479 1024,
8480 &http::HeaderMap::new(),
8481 )
8482 .unwrap();
8483 assert_eq!(&message[..], b"only");
8484 assert!(trailers.is_empty());
8485 }
8486}