typesafe_sdk/transport/mod.rs
1//! The seam between this crate and whatever sends the bytes.
2//!
3//! The SDK drives a `tower`-shaped service rather than an HTTP client, so a
4//! caller can put their own stack underneath it - a recorder, a proxy, an
5//! in-memory harness - without this crate knowing about it, and the default
6//! stack, [`HyperTransport`], is one implementation of that seam rather than a
7//! hard dependency. Any [`tower_service::Service`] over `http` requests with
8//! this crate's [`Body`] is a transport; [`HttpService`] names the bounds.
9//!
10//! The request body is this crate's own type implementing `http-body`, not a
11//! type borrowed from a pre-1.0 crate, so the public signature does not commit
12//! a caller to a version of somebody else's dependency.
13//!
14//! A transport sends one request. Everything around that - the headers every
15//! request carries, the deadline, reading the response under the size limit,
16//! turning a failure into this crate's [`Error`] - happens here, once per
17//! attempt, whichever transport is underneath.
18
19mod hyper;
20
21use std::{
22 convert::Infallible,
23 error::Error as StdError,
24 fmt,
25 future::{Future, poll_fn},
26 pin::Pin,
27 task::{Context, Poll},
28 time::Duration,
29};
30
31use bytes::Bytes;
32use http::{
33 HeaderMap, HeaderName, HeaderValue, Method, Request, Response, StatusCode, Uri,
34 header::CONTENT_TYPE,
35};
36use http_body::{Frame, SizeHint};
37use http_body_util::{BodyExt as _, LengthLimitError, Limited};
38use tower_service::Service;
39
40pub(crate) use self::hyper::TransportSettings;
41pub use self::hyper::{HttpVersion, HyperResponseFuture, HyperTransport, ResponseBody};
42use crate::{
43 config::Config,
44 constants::{
45 JSON_CONTENT_TYPE, PROTECTED_HEADERS, RETRY_COUNT_HEADER, RUNTIME_IDENTIFIER,
46 SDK_IDENTIFIER, TRANSPORT_HEADERS,
47 },
48 error::{ApiError, Error, format_endpoint},
49 question::upsert,
50 telemetry,
51 text::{self, Backslash, SafeText},
52};
53
54/// The error type a transport may fail with: any error that can cross
55/// threads.
56pub type BoxError = Box<dyn StdError + Send + Sync>;
57
58// ------------------------------------------------------------------- Body
59
60/// The body of a request the SDK sends: finished bytes, handed over in one
61/// frame.
62///
63/// Its length is known before the first byte is sent, so `size_hint` is exact
64/// and a transport can write a `Content-Length` without buffering. Cloning it
65/// shares the bytes rather than copying them.
66///
67/// `Debug` prints the length only: a body carries the caller's `state`, which
68/// may be personal data.
69#[derive(Clone, Default)]
70pub struct Body {
71 /// `None` once the one frame has been handed out, or for an empty body.
72 data: Option<Bytes>,
73}
74
75impl Body {
76 /// A body with no bytes, as a `GET` carries.
77 #[must_use]
78 pub fn empty() -> Self {
79 Self { data: None }
80 }
81
82 /// The bytes not yet handed out as a frame.
83 #[must_use]
84 pub fn len(&self) -> usize {
85 self.data.as_ref().map_or(0, Bytes::len)
86 }
87
88 /// Whether no bytes are left to hand out.
89 #[must_use]
90 pub fn is_empty(&self) -> bool {
91 self.len() == 0
92 }
93}
94
95impl From<Bytes> for Body {
96 fn from(bytes: Bytes) -> Self {
97 // An empty buffer is no frame at all, so that `is_end_stream` is true
98 // from the start and a transport sends no empty DATA frame.
99 Self { data: (!bytes.is_empty()).then_some(bytes) }
100 }
101}
102
103impl fmt::Debug for Body {
104 fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
105 formatter.debug_struct("Body").field("len", &self.len()).finish()
106 }
107}
108
109impl http_body::Body for Body {
110 type Data = Bytes;
111 type Error = Infallible;
112
113 fn poll_frame(
114 self: Pin<&mut Self>,
115 _: &mut Context<'_>,
116 ) -> Poll<Option<Result<Frame<Bytes>, Infallible>>> {
117 // `Body` holds nothing that cares where it lives in memory, so the
118 // pinned reference can be turned back into a plain one.
119 Poll::Ready(self.get_mut().data.take().map(|bytes| Ok(Frame::data(bytes))))
120 }
121
122 fn is_end_stream(&self) -> bool {
123 self.data.is_none()
124 }
125
126 fn size_hint(&self) -> SizeHint {
127 SizeHint::with_exact(self.len() as u64)
128 }
129}
130
131// ------------------------------------------------------------ HttpService
132
133mod sealed {
134 /// Keeps [`HttpService`](super::HttpService) implemented only through its
135 /// blanket implementation.
136 pub trait Sealed {}
137}
138
139/// What a transport has to be: a `tower` service that takes an `http` request
140/// with this crate's [`Body`] and answers with an `http` response.
141///
142/// It is implemented for every [`tower_service::Service`] that fits, and for
143/// nothing else, so it is a name for a set of bounds rather than a trait to
144/// implement: implement `Service` and a type is a transport. The service is
145/// cloned for every request and driven with its own `poll_ready`, so a
146/// service with back-pressure keeps it; the default [`HyperTransport`] is
147/// always ready.
148///
149/// The error of the service, and of its response body, is anything that can
150/// cross threads. It is kept as the [`source`](StdError::source) of the
151/// connection [`Error`] the call fails with.
152pub trait HttpService: sealed::Sealed + Clone + Send + Sync + 'static {
153 /// The body of the responses the service answers with.
154 type ResponseBody: http_body::Body<Data: Send, Error: Into<BoxError>> + Send + 'static;
155 /// What the service fails with when it cannot produce a response.
156 type Error: Into<BoxError>;
157 /// The future a call returns.
158 type Future: Future<Output = Result<Response<Self::ResponseBody>, Self::Error>> + Send;
159
160 /// [`Service::poll_ready`], forwarded.
161 ///
162 /// # Errors
163 ///
164 /// Returns the service's error when it can take no more requests.
165 fn poll_ready(&mut self, cx: &mut Context<'_>) -> Poll<Result<(), Self::Error>>;
166
167 /// [`Service::call`], forwarded.
168 fn call(&mut self, request: Request<Body>) -> Self::Future;
169}
170
171impl<S, B> sealed::Sealed for S
172where
173 S: Service<Request<Body>, Response = Response<B>> + Clone + Send + Sync + 'static,
174 S::Error: Into<BoxError>,
175 S::Future: Send,
176 B: http_body::Body<Data: Send, Error: Into<BoxError>> + Send + 'static,
177{
178}
179
180impl<S, B> HttpService for S
181where
182 S: Service<Request<Body>, Response = Response<B>> + Clone + Send + Sync + 'static,
183 S::Error: Into<BoxError>,
184 S::Future: Send,
185 B: http_body::Body<Data: Send, Error: Into<BoxError>> + Send + 'static,
186{
187 type ResponseBody = B;
188 type Error = S::Error;
189 type Future = S::Future;
190
191 fn poll_ready(&mut self, cx: &mut Context<'_>) -> Poll<Result<(), S::Error>> {
192 Service::poll_ready(self, cx)
193 }
194
195 fn call(&mut self, request: Request<Body>) -> S::Future {
196 Service::call(self, request)
197 }
198}
199
200// ------------------------------------------------------- header assembly
201
202/// The headers every request of one kind carries, built once per client.
203///
204/// Precedence, lowest first: the client's default headers, then the SDK's
205/// own, which no default can replace, and, when the request has a body,
206/// `Content-Type: application/json`. A default `X-TypeSafe-Retry-Count` is
207/// dropped: the SDK sets that header on retries and only there. So is a
208/// default framing or connection header ([`TRANSPORT_HEADERS`]). Per-call
209/// headers are applied on top of this map for each attempt; see
210/// [`call_headers`]. `User-Agent` carries the value the configuration built
211/// once, and `X-TypeSafe-Runtime` is left out when the configuration says so;
212/// a caller's header of either name is dropped all the same.
213pub(crate) fn base_headers(config: &Config, with_body: bool) -> HeaderMap {
214 let defaults = config.default_headers();
215 // Room for every default, the protected five and `Content-Type`, so
216 // building the map never grows it.
217 let mut headers = HeaderMap::with_capacity(defaults.len() + PROTECTED_HEADERS.len() + 1);
218 for (name, value) in defaults {
219 if !is_sdk_owned(name, with_body) {
220 headers.append(name, value.clone());
221 }
222 }
223 let [authorization, accept, user_agent, sdk, runtime] = PROTECTED_HEADERS;
224 headers.insert(authorization, config.authorization().clone());
225 headers.insert(accept, JSON_CONTENT_TYPE);
226 headers.insert(user_agent, config.user_agent().clone());
227 headers.insert(sdk, SDK_IDENTIFIER);
228 if config.send_runtime_header() {
229 headers.insert(runtime, RUNTIME_IDENTIFIER.clone());
230 }
231 if with_body {
232 headers.insert(CONTENT_TYPE, JSON_CONTENT_TYPE);
233 }
234 headers
235}
236
237/// Whether the SDK or its transport owns `name` on a request, so a caller
238/// cannot set it.
239fn is_sdk_owned(name: &HeaderName, with_body: bool) -> bool {
240 PROTECTED_HEADERS.contains(name)
241 || *name == RETRY_COUNT_HEADER
242 || (with_body && *name == CONTENT_TYPE)
243 || TRANSPORT_HEADERS.contains(name)
244}
245
246/// Parses the headers a caller set on one call, dropping the ones the SDK
247/// owns.
248///
249/// A later header of a name replaces an earlier one, as a later key does in a
250/// Python mapping. The protected headers, `X-TypeSafe-Retry-Count`, and
251/// `Content-Type` on a request with a body are dropped without an error, as
252/// the Python SDK overrides them. The framing and connection headers
253/// ([`TRANSPORT_HEADERS`]) are dropped the same way: they belong to the
254/// transport. `Host` is kept.
255///
256/// # Errors
257///
258/// Returns an [`ErrorKind::InvalidRequest`](crate::ErrorKind::InvalidRequest)
259/// error when a name is not a valid header name or a value is not a valid
260/// header value. The message names the header and never repeats a value.
261pub(crate) fn call_headers<'a, I>(
262 raw: I,
263 with_body: bool,
264) -> Result<Vec<(HeaderName, HeaderValue)>, Error>
265where
266 I: IntoIterator<Item = (&'a str, &'a str)>,
267 I::IntoIter: ExactSizeIterator,
268{
269 let raw = raw.into_iter();
270 let mut parsed: Vec<(HeaderName, HeaderValue)> = Vec::with_capacity(raw.len());
271 for (name, value) in raw {
272 let (name, value) = parse_header(name, value, "").map_err(Error::invalid_request)?;
273 if is_sdk_owned(&name, with_body) {
274 continue;
275 }
276 upsert(&mut parsed, name, value);
277 }
278 Ok(parsed)
279}
280
281/// Parses one header, or says which part of it is not valid.
282///
283/// The message names the header by its name - escaped, and cut at 128
284/// characters, since a name that fails here can be anything - and never
285/// repeats the value: a value is where a caller puts a token. `whose` is the word before `header` in the message: `default ` for
286/// a client default, empty for a per-call header.
287pub(crate) fn parse_header(
288 name: &str,
289 value: &str,
290 whose: &str,
291) -> Result<(HeaderName, HeaderValue), String> {
292 let parsed = HeaderName::from_bytes(name.as_bytes()).map_err(|_| {
293 format!("The {whose}header name {} is not a valid HTTP header name.", text::quoted(name))
294 })?;
295 let value = HeaderValue::from_str(value).map_err(|_| {
296 format!(
297 "The value of the {whose}header {} is not a valid HTTP header value.",
298 text::quoted(name)
299 )
300 })?;
301 Ok((parsed, value))
302}
303
304// ------------------------------------------------------------ one attempt
305
306/// Everything one request needs that does not change between its attempts.
307#[derive(Clone, Copy)]
308pub(crate) struct Exchange<'a> {
309 pub(crate) method: &'a Method,
310 pub(crate) uri: &'a Uri,
311 /// The client's headers for this kind of request; see [`base_headers`].
312 pub(crate) base_headers: &'a HeaderMap,
313 /// The call's own headers, already parsed; see [`call_headers`].
314 pub(crate) call_headers: &'a [(HeaderName, HeaderValue)],
315 /// The deadline of one attempt, or `None` for no deadline.
316 pub(crate) deadline: Option<Duration>,
317 pub(crate) max_response_bytes: usize,
318}
319
320/// What a successful attempt returns: a success status, the headers and the
321/// whole body.
322type Received = (StatusCode, HeaderMap, Bytes);
323
324/// Sends one attempt of a request and reads its response.
325///
326/// `retry` is how many attempts came before this one: from 1 it is sent as
327/// `X-TypeSafe-Retry-Count`, and at 0 the header is absent. `body` is the
328/// finished request body, handed over by value so that a first attempt costs
329/// no copy and a later one only a reference count.
330///
331/// The deadline covers the whole attempt: waiting for the transport to be
332/// ready, connecting, sending, and reading the response body.
333///
334/// # Errors
335///
336/// - [`ErrorKind::Timeout`](crate::ErrorKind::Timeout) when the deadline
337/// passes first.
338/// - [`ErrorKind::Api`](crate::ErrorKind::Api) for any status outside 2xx.
339/// - [`ErrorKind::Connection`](crate::ErrorKind::Connection) when the
340/// transport fails or the body cannot be read; the transport's own error is
341/// the [`source`](StdError::source).
342/// - [`ErrorKind::ResponseTooLarge`](crate::ErrorKind::ResponseTooLarge) when
343/// a success response's body is larger than the limit. A body over the
344/// limit is not read past it, and a failure response whose body is over the
345/// limit is an API error with the status and headers and no body.
346pub(crate) async fn attempt<S>(
347 service: &S,
348 exchange: Exchange<'_>,
349 retry: u32,
350 body: Option<Bytes>,
351) -> Result<Received, Error>
352where
353 S: HttpService,
354{
355 let events = telemetry::Exchange::new(exchange.method, exchange.uri, retry);
356 // The header map and the request are built inside a block so that their
357 // storage ends before the await. An async function keeps every local of
358 // a scope that is still open when it suspends, even one whose value was
359 // moved out, so written at the top level the two would ride in the
360 // future beside the copy the transport call already holds: 352 bytes of
361 // every call's future, which over a custom transport tips it over the
362 // size at which tokio boxes a spawned future in a debug build (2,048
363 // bytes). Over the default transport hyper's response future keeps the
364 // call over that size either way (2,344 bytes), so a debug-build call
365 // spawned on the default client is boxed.
366 let (started, exchanged) = {
367 let mut headers = exchange.base_headers.clone();
368 for (name, value) in exchange.call_headers {
369 headers.insert(name.clone(), value.clone());
370 }
371 if retry > 0 {
372 headers.insert(RETRY_COUNT_HEADER, HeaderValue::from(retry));
373 }
374 telemetry::sending(events, &headers, body.as_ref());
375 let started = telemetry::clock();
376
377 let mut request = Request::new(body.map_or_else(Body::empty, Body::from));
378 *request.method_mut() = exchange.method.clone();
379 *request.uri_mut() = exchange.uri.clone();
380 *request.headers_mut() = headers;
381 (started, exchange_once(service, request, exchange.max_response_bytes))
382 };
383 let outcome = match exchange.deadline {
384 Some(deadline) => tokio::time::timeout(deadline, exchanged)
385 .await
386 .unwrap_or_else(|_| Err(Failure::Error(Error::timeout(deadline)))),
387 None => exchanged.await,
388 };
389
390 // A response is reported by its status; an attempt that ended without
391 // one is reported by what ended it. An API error is not reported a second
392 // time as a failure: its message can come from the body.
393 let failure = match outcome {
394 Ok((status, headers, body)) => {
395 telemetry::responded(events, status, &headers, started);
396 telemetry::received(events, status, &headers, &body, started);
397 if status.is_success() {
398 return Ok((status, headers, body));
399 }
400 return Err(ApiError::new(status, body, headers, Some(endpoint(exchange))).into());
401 }
402 Err(Failure::TooLarge { status, headers }) if !status.is_success() => {
403 telemetry::responded(events, status, &headers, started);
404 return Err(ApiError::with_message(
405 status,
406 Bytes::new(),
407 headers,
408 Some(endpoint(exchange)),
409 Error::response_too_large(exchange.max_response_bytes).to_string(),
410 )
411 .into());
412 }
413 Err(Failure::TooLarge { .. }) => Error::response_too_large(exchange.max_response_bytes),
414 Err(Failure::Error(error)) => error,
415 };
416 telemetry::failed(events, &failure, started);
417 Err(failure)
418}
419
420/// How an attempt can end short of a response body.
421enum Failure {
422 /// Anything that becomes an [`Error`] without needing the response.
423 Error(Error),
424 /// The body was larger than the limit. Whether that is a response too
425 /// large or an API error depends on the status, which is kept.
426 TooLarge { status: StatusCode, headers: HeaderMap },
427}
428
429/// Waits for the transport, sends `request`, and reads the whole response.
430async fn exchange_once<S>(
431 service: &S,
432 request: Request<Body>,
433 limit: usize,
434) -> Result<Received, Failure>
435where
436 S: HttpService,
437{
438 // A clone per call, as `tower` intends: readiness belongs to the handle
439 // that is then called, and a shared one would let another task take the
440 // slot this one waited for. The handle is dropped once the call has been
441 // made: the response future owns what it needs, and a handle kept to the
442 // end would be stored in this future through the whole body read.
443 let called = {
444 let mut service = service.clone();
445 poll_fn(|cx| service.poll_ready(cx))
446 .await
447 .map_err(|error| Failure::Error(connection(error)))?;
448 service.call(request)
449 };
450 let response = called.await.map_err(|error| Failure::Error(connection(error)))?;
451 let (parts, body) = response.into_parts();
452
453 // A declared length over the limit is refused before a byte is read.
454 if http_body::Body::size_hint(&body).lower() > limit as u64 {
455 return Err(Failure::TooLarge { status: parts.status, headers: parts.headers });
456 }
457 match Limited::new(body, limit).collect().await {
458 Ok(collected) => Ok((parts.status, parts.headers, collected.to_bytes())),
459 Err(error) if error.is::<LengthLimitError>() => {
460 Err(Failure::TooLarge { status: parts.status, headers: parts.headers })
461 }
462 Err(error) => Err(Failure::Error(connection(error))),
463 }
464}
465
466/// The error a transport failure becomes.
467///
468/// An error this crate raised inside its own transport is passed through as
469/// it is. Anything else is a connection failure whose message is the chain of
470/// the transport's own messages - the cause stays reachable as the
471/// [`source`](StdError::source).
472fn connection(error: impl Into<BoxError>) -> Error {
473 match error.into().downcast::<Error>() {
474 Ok(ours) => *ours,
475 Err(other) => Error::connection(connection_message(&*other), Some(other)),
476 }
477}
478
479/// `Connection error: ` and then every message of the error chain, joined
480/// with `: `.
481///
482/// The chain is the transport's, not the request's: it names what failed -
483/// `tcp connect error: Connection refused`, `invalid peer certificate:
484/// UnknownIssuer` - and holds neither a header nor a body. It is cut after
485/// eight links, which no real chain reaches.
486///
487/// It can still hold text the server chose: an HTTP/2 GOAWAY's debug data
488/// (up to a frame, 16 KiB by default), the subject of a certificate the
489/// platform refused, and whatever a caller's own transport puts in its
490/// `Display`. So every link is written as text this SDK did not write -
491/// control and format characters escaped - and the whole of it after the
492/// prefix is cut at [`text::MAX_MESSAGE_CHARS`] characters and marked
493/// with U+2026; the full chain stays reachable through
494/// [`source`](StdError::source). A backslash is written as it is: h2 and
495/// rustls already print their own text through `Debug`, where a doubled
496/// backslash would only make an escape harder to read, and nothing is ever
497/// parsed back out of this message.
498fn connection_message(error: &(dyn StdError + 'static)) -> String {
499 use fmt::Write as _;
500
501 let mut message = SafeText::after(
502 String::from("Connection error: "),
503 text::MAX_MESSAGE_CHARS,
504 Backslash::Keep,
505 );
506 let mut link = Some(error);
507 for index in 0..8 {
508 let Some(current) = link else { break };
509 if index > 0 {
510 message.fixed(": ");
511 }
512 write!(message.untrusted_writer(), "{current}")
513 .expect("invariant: the escaping writer never fails");
514 link = current.source();
515 }
516 message.into_string()
517}
518
519/// The endpoint of `exchange` as an error names it.
520fn endpoint(exchange: Exchange<'_>) -> Box<str> {
521 format_endpoint(exchange.method, exchange.uri).into_boxed_str()
522}
523
524#[cfg(test)]
525#[path = "mod_tests.rs"]
526mod tests;