Skip to main content

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;