Skip to main content

typesafe_sdk/
error.rs

1//! What a call can fail with, and how a failure renders.
2//!
3//! [`Error`] is the one error type every fallible operation returns. It is a
4//! single boxed pointer, so `Result<T, Error>` costs a pointer beside `T`
5//! rather than the size of the largest failure; the detail lives behind
6//! [`Error::kind`].
7//!
8//! Four things here are not what a plain `thiserror` enum would do:
9//!
10//! * **An API failure's message is extracted from the server's body in a fixed
11//!   order.** Servers put the human-readable sentence under `error`,
12//!   `error.message`, `message`, `detail`, `detail.message` or a list under
13//!   `detail`, and the SDK reads them in that order so the message a caller
14//!   sees does not depend on which shape the endpoint chose. See
15//!   [`ApiError::message`].
16//! * **Nothing here prints a header value or a codec error's input.** An
17//!   `Authorization` header and a decode error's excerpt of the body are both
18//!   in reach of these types, and neither appears in `Debug` or `Display`.
19//! * **Text the server chose is escaped and cut before it is printed.** An
20//!   API failure's message and the request id are the server's text, and
21//!   either would otherwise put a line break, a terminal colour or 16 MiB into
22//!   every log line that prints the error. The accessors for the body and the
23//!   headers still return them as they arrived.
24//! * **`Retry-After` is parsed against a caller-supplied `now`.** Reading the
25//!   clock inside the parser would make the HTTP-date case untestable without
26//!   mocking time, so the parser takes the instant to measure against and
27//!   [`ApiError::retry_after`] is the thin wrapper that reads the clock.
28
29use std::{
30    borrow::Cow,
31    error::Error as StdError,
32    fmt,
33    time::{Duration, SystemTime},
34};
35
36use bytes::Bytes;
37use http::{HeaderMap, Method, StatusCode, Uri, header};
38use serde::{Deserialize, de::IgnoredAny};
39
40use crate::{
41    codec::{self, DecodeError, DecodeErrorKind, RawJson},
42    constants::{RETRY_AFTER_MS_HEADER, request_id},
43    text,
44};
45
46/// What a failure this crate could not attribute to itself was caused by.
47type Cause = Box<dyn StdError + Send + Sync>;
48
49// ------------------------------------------------------------------- Error
50
51/// Anything a call to the API can fail with.
52///
53/// The type is one pointer wide whatever the failure was, so a `Result` from
54/// this crate is the size of its success value plus a pointer. Match on
55/// [`kind`](Error::kind) to tell the failures apart; the `Display` of this
56/// type is the sentence to show a user, and [`source`](StdError::source)
57/// leads to the failure underneath when there was one.
58pub struct Error(Box<Inner>);
59
60/// The heap half of [`Error`], so that the stack half stays a pointer.
61struct Inner {
62    kind: ErrorKind,
63    /// The sentence for the kinds that do not carry a payload of their own.
64    /// Empty for [`ErrorKind::Api`], [`ErrorKind::ResponseValidation`],
65    /// [`ErrorKind::Timeout`] and [`ErrorKind::ResponseTooLarge`], which render
66    /// from their payload instead.
67    message: Box<str>,
68    source: Option<Cause>,
69}
70
71/// Which kind of failure an [`Error`] is.
72///
73/// New variants are added as the SDK learns to distinguish more failures, so
74/// a `match` over this enum needs a catch-all arm.
75#[derive(Debug)]
76#[non_exhaustive]
77pub enum ErrorKind {
78    /// The client could not be built: a missing or malformed API key, a base
79    /// URL that is not a URL, a timeout that is zero or not finite.
80    Config,
81    /// The request could not be built from what the caller supplied, and was
82    /// never sent.
83    InvalidRequest,
84    /// The server answered, and the answer was not a success status.
85    Api(ApiError),
86    /// The request never produced an HTTP response: the connection failed,
87    /// was refused, or was lost mid-flight.
88    Connection,
89    /// The attempt exceeded its deadline.
90    Timeout {
91        /// The deadline the attempt was given.
92        timeout: Duration,
93    },
94    /// The server answered with a success status and a body this SDK could
95    /// not read as the response it expected.
96    ResponseValidation(ResponseValidationError),
97    /// The server answered with a success status and a body larger than the
98    /// client's limit, so the body was not read past the limit and nothing
99    /// was decoded.
100    ///
101    /// The limit is 16 MiB unless
102    /// [`ClientBuilder::max_response_bytes`](crate::ClientBuilder::max_response_bytes)
103    /// set another. Retrying the same request cannot help: the answer will be
104    /// as large again. A failure status with a body over the limit is an
105    /// [`ErrorKind::Api`] instead, which keeps its status and headers.
106    ResponseTooLarge {
107        /// The limit the body exceeded, in bytes.
108        limit: usize,
109    },
110}
111
112impl Error {
113    /// Which kind of failure this is.
114    pub fn kind(&self) -> &ErrorKind {
115        &self.0.kind
116    }
117
118    /// The client could not be built from the configuration it was given.
119    pub(crate) fn config(message: impl Into<Box<str>>) -> Self {
120        Self::plain(ErrorKind::Config, message, None)
121    }
122
123    /// The caller's arguments do not describe a request that can be sent.
124    pub(crate) fn invalid_request(message: impl Into<Box<str>>) -> Self {
125        Self::plain(ErrorKind::InvalidRequest, message, None)
126    }
127
128    /// The request failed without an HTTP response.
129    ///
130    /// `cause` is the transport's own error, kept as the
131    /// [`source`](StdError::source) so a caller can downcast to it.
132    pub(crate) fn connection(message: impl Into<Box<str>>, cause: Option<Cause>) -> Self {
133        Self::plain(ErrorKind::Connection, message, cause)
134    }
135
136    /// The attempt ran past `timeout`.
137    pub(crate) fn timeout(timeout: Duration) -> Self {
138        Self::plain(ErrorKind::Timeout { timeout }, "", None)
139    }
140
141    /// A success response's body was larger than `limit` bytes.
142    pub(crate) fn response_too_large(limit: usize) -> Self {
143        Self::plain(ErrorKind::ResponseTooLarge { limit }, "", None)
144    }
145
146    /// Builds one of the kinds whose sentence is not derived from a payload.
147    fn plain(kind: ErrorKind, message: impl Into<Box<str>>, source: Option<Cause>) -> Self {
148        Self(Box::new(Inner { kind, message: message.into(), source }))
149    }
150}
151
152impl From<ApiError> for Error {
153    fn from(error: ApiError) -> Self {
154        Self::plain(ErrorKind::Api(error), "", None)
155    }
156}
157
158impl From<ResponseValidationError> for Error {
159    fn from(error: ResponseValidationError) -> Self {
160        Self::plain(ErrorKind::ResponseValidation(error), "", None)
161    }
162}
163
164impl fmt::Display for Error {
165    fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
166        match &self.0.kind {
167            ErrorKind::Api(error) => error.fmt(formatter),
168            ErrorKind::ResponseValidation(error) => error.fmt(formatter),
169            ErrorKind::Timeout { timeout } => {
170                write!(formatter, "Request timed out (timeout={}s).", timeout.as_secs_f64())
171            }
172            ErrorKind::ResponseTooLarge { limit } => write!(
173                formatter,
174                "The response body exceeded the limit of {limit} bytes and was not read."
175            ),
176            ErrorKind::Config | ErrorKind::InvalidRequest | ErrorKind::Connection => {
177                formatter.write_str(&self.0.message)
178            }
179        }
180    }
181}
182
183impl fmt::Debug for Error {
184    fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
185        let mut shown = formatter.debug_struct("Error");
186        shown.field("kind", &self.0.kind);
187        if !self.0.message.is_empty() {
188            shown.field("message", &self.0.message);
189        }
190        if let Some(source) = &self.0.source {
191            shown.field("source", source);
192        }
193        shown.finish()
194    }
195}
196
197impl StdError for Error {
198    /// The failure underneath this one, when there is one.
199    ///
200    /// An [`ErrorKind::Api`] has none: the server's answer is the failure, and
201    /// it is already what `Display` prints. A response-validation failure
202    /// leads to the decode error that names the offending field, which carries
203    /// a position `Display` leaves out.
204    fn source(&self) -> Option<&(dyn StdError + 'static)> {
205        match &self.0.kind {
206            ErrorKind::Api(_) => None,
207            ErrorKind::ResponseValidation(error) => Some(error.decode_error()),
208            _ => self.0.source.as_ref().map(|cause| &**cause as &(dyn StdError + 'static)),
209        }
210    }
211}
212
213// ---------------------------------------------------------------- ApiError
214
215/// Which class of API failure a status code puts a response in.
216///
217/// The mapping is by status alone, so an endpoint answering a documented
218/// status with an undocumented body still lands in the right class.
219#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
220#[non_exhaustive]
221pub enum ApiErrorKind {
222    /// 400: the request was malformed.
223    BadRequest,
224    /// 401: the API key was missing, malformed or rejected.
225    Authentication,
226    /// 403: the key is valid and is not allowed to do this.
227    PermissionDenied,
228    /// 404: no such endpoint or resource.
229    NotFound,
230    /// 422: the request parsed and failed the server's validation.
231    UnprocessableEntity,
232    /// 429: the rate limit was exceeded. [`ApiError::retry_after`] may say
233    /// how long to wait.
234    RateLimit,
235    /// Any status at or above 500: the server failed to answer the request.
236    InternalServer,
237    /// Any other unsuccessful status.
238    Other,
239}
240
241impl ApiErrorKind {
242    /// The class `status` falls in.
243    fn of(status: StatusCode) -> Self {
244        match status.as_u16() {
245            400 => Self::BadRequest,
246            401 => Self::Authentication,
247            403 => Self::PermissionDenied,
248            404 => Self::NotFound,
249            422 => Self::UnprocessableEntity,
250            429 => Self::RateLimit,
251            500.. => Self::InternalServer,
252            _ => Self::Other,
253        }
254    }
255}
256
257/// An unsuccessful HTTP response, with the body and metadata it came with.
258///
259/// Reach it through [`ErrorKind::Api`].
260#[derive(Clone)]
261pub struct ApiError {
262    status: StatusCode,
263    headers: HeaderMap,
264    body: Bytes,
265    endpoint: Option<Box<str>>,
266    message: Box<str>,
267    error_type: Option<Box<str>>,
268}
269
270impl ApiError {
271    /// Builds the error for a response the server refused, reading its message
272    /// out of `body`.
273    pub(crate) fn new(
274        status: StatusCode,
275        body: Bytes,
276        headers: HeaderMap,
277        endpoint: Option<Box<str>>,
278    ) -> Self {
279        let reading = BodyReading::of(&body);
280        Self {
281            status,
282            headers,
283            body,
284            endpoint,
285            message: reading.message,
286            error_type: reading.error_type,
287        }
288    }
289
290    /// The same, with a message the caller supplies instead of the one the
291    /// body would give.
292    pub(crate) fn with_message(
293        status: StatusCode,
294        body: Bytes,
295        headers: HeaderMap,
296        endpoint: Option<Box<str>>,
297        message: impl Into<Box<str>>,
298    ) -> Self {
299        let reading = BodyReading::of(&body);
300        Self {
301            status,
302            headers,
303            body,
304            endpoint,
305            message: message.into(),
306            error_type: reading.error_type,
307        }
308    }
309
310    /// The response status.
311    pub fn status(&self) -> StatusCode {
312        self.status
313    }
314
315    /// Which class of failure the status puts this response in.
316    pub fn kind(&self) -> ApiErrorKind {
317        ApiErrorKind::of(self.status)
318    }
319
320    /// The response headers, as they arrived.
321    pub fn headers(&self) -> &HeaderMap {
322        &self.headers
323    }
324
325    /// The server's identifier for this request, from `x-typesafe-request-id`.
326    ///
327    /// `None` when the header is absent or is not text. This is the header's
328    /// text exactly as it arrived; `Display` and `Debug` show it escaped and
329    /// cut at 128 characters instead.
330    pub fn request_id(&self) -> Option<&str> {
331        request_id(&self.headers)
332    }
333
334    /// The method and URL the request went to, without credentials, query or
335    /// fragment, as `GET https://api.typesafe.ai/v1/models`.
336    ///
337    /// `None` when the failure was built without a request to name.
338    pub fn endpoint(&self) -> Option<&str> {
339        self.endpoint.as_deref()
340    }
341
342    /// The sentence the server gave for this failure.
343    ///
344    /// It is read from the body at the first of these that holds a string:
345    /// `error`, `error.message`, `message`, `detail`, `detail.message`, or a
346    /// list under `detail` whose entries are joined with `; ` as
347    /// `<loc>: <msg>`. Failing all of those, it is the body itself, compacted
348    /// when it is JSON; an empty body, or a body that is the JSON `null`,
349    /// gives `status code (no body)`.
350    ///
351    /// Whichever part of the body it came from, the text is the server's, so
352    /// it is made safe to print: a control character, or a format character
353    /// that reorders or hides the text around it, is written as a Rust escape
354    /// (`\n`, `\u{1b}`), and the text is cut at 200 characters, counted after
355    /// escaping, and marked with U+2026. The Python SDK keeps a member's text
356    /// as the server sent it and cuts only a body standing in for a message;
357    /// here every path is cut, so a server cannot break, recolour or flood
358    /// the log line of a caller that prints the error.
359    /// [`body_text`](Self::body_text) still returns every byte.
360    ///
361    /// It can be empty, which is how a caller-supplied empty message survives
362    /// to `Display`, where the status then stands alone.
363    pub fn message(&self) -> &str {
364        &self.message
365    }
366
367    /// The server's machine-readable name for this failure, from
368    /// `detail.error_type`.
369    ///
370    /// The live API answers a request with no key with 403 and
371    /// `authentication_error` here, which is the only way to tell that case
372    /// apart from a key that exists and lacks a permission.
373    ///
374    /// This is the server's text as it arrived, for a caller to compare. It is
375    /// never part of `Display`; `Debug` shows it escaped and cut at 128
376    /// characters, as it does the request id.
377    pub fn error_type(&self) -> Option<&str> {
378        self.error_type.as_deref()
379    }
380
381    /// The response body, exactly as it arrived.
382    pub fn body(&self) -> &[u8] {
383        &self.body
384    }
385
386    /// The response body as text, with anything that is not UTF-8 replaced by
387    /// `U+FFFD`.
388    pub fn body_text(&self) -> Cow<'_, str> {
389        String::from_utf8_lossy(&self.body)
390    }
391
392    /// The response body, decoded as JSON.
393    ///
394    /// # Errors
395    ///
396    /// Returns a [`DecodeError`] when the body is not JSON, is nested deeper
397    /// than this crate's limit, or does not fit `T`.
398    pub fn body_json<'de, T>(&'de self) -> Result<T, DecodeError>
399    where
400        T: Deserialize<'de>,
401    {
402        codec::decode(&self.body)
403    }
404
405    /// How long the server asked the caller to wait, from `retry-after-ms` or
406    /// `Retry-After`.
407    ///
408    /// Read for any status, not only 429: a 503 may carry the same headers.
409    /// Reads the system clock, because `Retry-After` may be an HTTP date.
410    pub fn retry_after(&self) -> Option<Duration> {
411        parse_retry_after(&self.headers, SystemTime::now())
412    }
413}
414
415impl fmt::Display for ApiError {
416    fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
417        render(formatter, self.endpoint(), self.status, &self.message, self.request_id())
418    }
419}
420
421impl fmt::Debug for ApiError {
422    /// Prints what identifies the failure, and nothing that could be a secret.
423    ///
424    /// The headers are reduced to their count and the body to its length: a
425    /// response carries back whatever the request sent under `Authorization`
426    /// or a cookie in some proxy configurations, and a `Debug` that is printed
427    /// into a log must not be the thing that puts it there.
428    fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
429        formatter
430            .debug_struct("ApiError")
431            .field("status", &self.status.as_u16())
432            .field("kind", &self.kind())
433            .field("endpoint", &self.endpoint())
434            .field("request_id", &self.request_id().map(shown_name))
435            .field("message", &self.message)
436            .field("error_type", &self.error_type().map(shown_name))
437            .field("headers", &HeaderCount(self.headers.len()))
438            .field("body", &ByteCount(self.body.len()))
439            .finish()
440    }
441}
442
443impl StdError for ApiError {}
444
445// ------------------------------------------------- ResponseValidationError
446
447/// A successful HTTP response whose body this SDK could not read.
448///
449/// Reach it through [`ErrorKind::ResponseValidation`]. The raw body is kept,
450/// so a caller can recover data this version of the SDK does not model.
451#[derive(Clone)]
452pub struct ResponseValidationError {
453    status: StatusCode,
454    headers: HeaderMap,
455    body: Bytes,
456    endpoint: Option<Box<str>>,
457    source: DecodeError,
458    message: Box<str>,
459}
460
461impl ResponseValidationError {
462    /// Builds the error for a body that did not fit the response type.
463    pub(crate) fn new(
464        status: StatusCode,
465        body: Bytes,
466        headers: HeaderMap,
467        endpoint: Option<Box<str>>,
468        source: DecodeError,
469    ) -> Self {
470        let message = format!("Invalid response data at '{}'.", source.path()).into_boxed_str();
471        Self { status, headers, body, endpoint, source, message }
472    }
473
474    /// The dotted path to the field that was missing or of the wrong type,
475    /// such as `answers.spam.noul`.
476    ///
477    /// Empty when the body failed before any field could be named, which is
478    /// what a syntax error or a document nested too deep gives.
479    pub fn field_path(&self) -> &str {
480        self.source.path()
481    }
482
483    /// The response status, which was a success status.
484    pub fn status(&self) -> StatusCode {
485        self.status
486    }
487
488    /// The response headers, as they arrived.
489    pub fn headers(&self) -> &HeaderMap {
490        &self.headers
491    }
492
493    /// The server's identifier for this request, from `x-typesafe-request-id`.
494    ///
495    /// The header's text exactly as it arrived; `Display` and `Debug` show it
496    /// escaped and cut at 128 characters instead.
497    pub fn request_id(&self) -> Option<&str> {
498        request_id(&self.headers)
499    }
500
501    /// The method and URL the request went to, without credentials, query or
502    /// fragment.
503    pub fn endpoint(&self) -> Option<&str> {
504        self.endpoint.as_deref()
505    }
506
507    /// The sentence for this failure, `Invalid response data at '<path>'.`.
508    pub fn message(&self) -> &str {
509        &self.message
510    }
511
512    /// The response body, exactly as it arrived.
513    pub fn body(&self) -> &[u8] {
514        &self.body
515    }
516
517    /// The response body as text, with anything that is not UTF-8 replaced by
518    /// `U+FFFD`.
519    pub fn body_text(&self) -> Cow<'_, str> {
520        String::from_utf8_lossy(&self.body)
521    }
522
523    /// The response body, decoded as JSON.
524    ///
525    /// This is how a caller recovers data the SDK's own response type dropped,
526    /// including whatever made the decode fail.
527    ///
528    /// # Errors
529    ///
530    /// Returns a [`DecodeError`] when the body is not JSON, is nested deeper
531    /// than this crate's limit, or does not fit `T`.
532    pub fn body_json<'de, T>(&'de self) -> Result<T, DecodeError>
533    where
534        T: Deserialize<'de>,
535    {
536        codec::decode(&self.body)
537    }
538
539    /// The decode failure underneath, which carries the position as well as
540    /// the path.
541    pub fn decode_error(&self) -> &DecodeError {
542        &self.source
543    }
544}
545
546impl fmt::Display for ResponseValidationError {
547    fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
548        render(formatter, self.endpoint(), self.status, &self.message, self.request_id())
549    }
550}
551
552impl fmt::Debug for ResponseValidationError {
553    /// Prints what identifies the failure, and nothing that could be a secret;
554    /// see the note on [`ApiError`]'s `Debug`.
555    fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
556        formatter
557            .debug_struct("ResponseValidationError")
558            .field("status", &self.status.as_u16())
559            .field("endpoint", &self.endpoint())
560            .field("request_id", &self.request_id().map(shown_name))
561            .field("field_path", &self.field_path())
562            .field("source", &self.source)
563            .field("headers", &HeaderCount(self.headers.len()))
564            .field("body", &ByteCount(self.body.len()))
565            .finish()
566    }
567}
568
569impl StdError for ResponseValidationError {
570    fn source(&self) -> Option<&(dyn StdError + 'static)> {
571        Some(&self.source)
572    }
573}
574
575// --------------------------------------------------------------- rendering
576
577/// Writes the one line an API-shaped failure renders as.
578///
579/// `<endpoint>: <status> <message> (request_id=<id>)`, with each optional part
580/// left out when it is absent. The status is the bare number, not the number
581/// and its reason phrase, so the line reads the same whether or not the status
582/// is one the `http` crate has a name for. The message arrives already made
583/// safe to print; the request id is the header's raw text and is made safe
584/// here.
585fn render(
586    formatter: &mut fmt::Formatter<'_>,
587    endpoint: Option<&str>,
588    status: StatusCode,
589    message: &str,
590    request_id: Option<&str>,
591) -> fmt::Result {
592    if let Some(endpoint) = endpoint {
593        write!(formatter, "{endpoint}: ")?;
594    }
595    write!(formatter, "{}", status.as_u16())?;
596    if !message.is_empty() {
597        write!(formatter, " {message}")?;
598    }
599    if let Some(request_id) = request_id {
600        write!(formatter, " (request_id={})", shown_name(request_id))?;
601    }
602    Ok(())
603}
604
605/// A name the server chose - the request id, the error type - as a message
606/// or a `Debug` shows it: escaped, and cut at 128 characters, as the SDK's log
607/// lines show the request id.
608///
609/// `http` hands a header value over as text only when it is visible ASCII and
610/// tabs, so for the request id this escapes the tabs and bounds the length; a
611/// body member can hold anything.
612fn shown_name(name: &str) -> String {
613    text::bounded(&name, text::MAX_NAME_CHARS)
614}
615
616/// Stands in for the headers in a `Debug`, so their values never reach it.
617struct HeaderCount(usize);
618
619impl fmt::Debug for HeaderCount {
620    fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
621        write!(formatter, "<{} redacted>", self.0)
622    }
623}
624
625/// Stands in for a body in a `Debug`, so its bytes never reach it.
626struct ByteCount(usize);
627
628impl fmt::Debug for ByteCount {
629    fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
630        write!(formatter, "<{} bytes>", self.0)
631    }
632}
633
634/// Renders the endpoint of a request as an error names it: the method, then
635/// the URL without userinfo, query or fragment.
636///
637/// A URL's userinfo is a credential and its query may carry one, so neither
638/// belongs in an error that will be logged. `http`'s `Uri` drops the fragment
639/// when it parses, so there is none left to strip. A port is kept only when it
640/// is not the default for the scheme, which is how the same endpoint reads the
641/// same whether or not the caller spelled `:443` out.
642pub(crate) fn format_endpoint(method: &Method, uri: &Uri) -> String {
643    let mut out = String::with_capacity(method.as_str().len() + 1 + uri.path().len() + 32);
644    out.push_str(method.as_str());
645    out.push(' ');
646    if let Some(scheme) = uri.scheme() {
647        out.push_str(scheme.as_str());
648        out.push_str("://");
649    }
650    if let Some(host) = uri.host() {
651        out.push_str(host);
652        if let Some(port) = uri.port_u16()
653            && default_port(uri.scheme_str()) != Some(port)
654        {
655            out.push(':');
656            out.push_str(&port.to_string());
657        }
658    }
659    out.push_str(uri.path());
660    out
661}
662
663/// The port a scheme implies, and so does not need spelling out.
664fn default_port(scheme: Option<&str>) -> Option<u16> {
665    match scheme {
666        Some("http") => Some(80),
667        Some("https") => Some(443),
668        _ => None,
669    }
670}
671
672// ------------------------------------------------------------ retry-after
673
674/// How long the server asked the caller to wait, from the response headers.
675///
676/// `retry-after-ms` is read first and is a count of milliseconds;
677/// `Retry-After` is read second and is either a count of seconds or an HTTP
678/// date, measured against `now`. A value the first header cannot supply falls
679/// through to the second, with one exception: a negative `Retry-After` ends
680/// the search, because a server asking for a wait in the past is asking for no
681/// wait at all and should not then be given the backoff a missing header would
682/// produce.
683///
684/// The result is truncated to whole milliseconds, which is the precision the
685/// millisecond header carries and far finer than any retry schedule needs.
686pub(crate) fn parse_retry_after(headers: &HeaderMap, now: SystemTime) -> Option<Duration> {
687    for (name, per_unit) in
688        [(RETRY_AFTER_MS_HEADER, 1.0_f64), (header::RETRY_AFTER.as_str(), 1000.0_f64)]
689    {
690        let Some(raw) = headers.get(name).and_then(|value| value.to_str().ok()) else {
691            continue;
692        };
693        let trimmed = raw.trim();
694        // An empty header is a request to wait no time at all, not a parse
695        // failure: `Retry-After:` with nothing after it means zero.
696        let spelled = if trimmed.is_empty() { "0" } else { trimmed };
697        match spelled.parse::<f64>() {
698            // A value that is not finite says nothing about how long to wait,
699            // so the next header gets its turn.
700            Ok(seconds) if !seconds.is_finite() => {}
701            Ok(seconds) if seconds >= 0.0 => {
702                let millis = seconds * per_unit;
703                if millis.is_finite() {
704                    return Some(millis_to_duration(millis));
705                }
706            }
707            Ok(_) if name == header::RETRY_AFTER.as_str() => return None,
708            Ok(_) => {}
709            Err(_) if name == header::RETRY_AFTER.as_str() => {
710                if let Ok(when) = httpdate::parse_http_date(raw) {
711                    // A date already past means the wait is over, not that the
712                    // header was unusable, so it answers zero rather than
713                    // falling through to a backoff.
714                    let wait = when.duration_since(now).unwrap_or(Duration::ZERO);
715                    return Some(Duration::from_millis(
716                        u64::try_from(wait.as_millis()).unwrap_or(u64::MAX),
717                    ));
718                }
719            }
720            Err(_) => {}
721        }
722    }
723    None
724}
725
726/// Turns a finite, non-negative count of milliseconds into a [`Duration`],
727/// saturating rather than wrapping on a value no `Duration` can hold.
728fn millis_to_duration(millis: f64) -> Duration {
729    // A float-to-integer cast saturates in Rust, so a value past `u64::MAX`
730    // lands on `u64::MAX` rather than wrapping to a short wait.
731    Duration::from_millis(millis as u64)
732}
733
734// ------------------------------------------------------- the error message
735
736/// What an error body says: the sentence for it, and the machine-readable
737/// name beside it.
738struct BodyReading {
739    message: Box<str>,
740    error_type: Option<Box<str>>,
741}
742
743/// The members of an error body this crate reads, each captured as raw JSON so
744/// that a member of an unexpected type costs a failed decode of that member
745/// rather than a failed decode of the whole body.
746#[derive(Deserialize)]
747struct Envelope {
748    error: Option<RawJson>,
749    message: Option<RawJson>,
750    detail: Option<RawJson>,
751}
752
753/// The two members read from an object under `detail`.
754#[derive(Deserialize)]
755struct Detail {
756    message: Option<RawJson>,
757    error_type: Option<RawJson>,
758}
759
760/// The one member read from an object under `error`.
761#[derive(Deserialize)]
762struct MessageMember {
763    message: Option<RawJson>,
764}
765
766/// One entry of a list under `detail`, in the shape a validation framework
767/// reports a field error.
768#[derive(Deserialize)]
769struct DetailEntry {
770    msg: Option<RawJson>,
771    loc: Option<RawJson>,
772}
773
774impl BodyReading {
775    /// Reads `body` the way the API's own SDKs do.
776    fn of(body: &[u8]) -> Self {
777        if body.is_empty() {
778            return Self::no_body();
779        }
780        match first_token(body) {
781            Some(b'{') => Self::of_object(body),
782            // A JSON string body is its own message. An empty one leaves the
783            // status to stand alone, which is what a caller-supplied empty
784            // message does too.
785            Some(b'"') => match codec::decode::<String>(body) {
786                Ok(text) => Self::said(bounded(&text)),
787                Err(_) => Self::said(text_message(body)),
788            },
789            // Everything else is a number, a boolean, a list or `null` - or it
790            // is not JSON at all, which only parsing can tell.
791            _ => match codec::decode::<Option<IgnoredAny>>(body) {
792                Ok(None) => Self::no_body(),
793                Ok(Some(_)) => Self::said(json_message(body)),
794                Err(failure) => Self::said(unparsed_message(body, &failure)),
795            },
796        }
797    }
798
799    /// The reading for a body that is empty, or is the JSON `null` that stands
800    /// for an empty one.
801    fn no_body() -> Self {
802        Self { message: "status code (no body)".into(), error_type: None }
803    }
804
805    /// The reading for a body with a message and nothing else to report.
806    fn said(message: impl Into<Box<str>>) -> Self {
807        Self { message: message.into(), error_type: None }
808    }
809
810    /// The object case, where the message can be in any of six places.
811    fn of_object(body: &[u8]) -> Self {
812        let envelope = match codec::decode::<Envelope>(body) {
813            Ok(envelope) => envelope,
814            Err(failure) => return Self::said(unparsed_message(body, &failure)),
815        };
816        let detail = envelope.detail.as_ref().and_then(|raw| raw.decode::<Detail>().ok());
817        let error_type = detail
818            .as_ref()
819            .and_then(|detail| detail.error_type.as_ref())
820            .and_then(as_text)
821            .map(Into::into);
822
823        let message = as_text_of(&envelope.error)
824            .or_else(|| {
825                member_text(&envelope.error, |raw| {
826                    raw.decode::<MessageMember>().ok().and_then(|it| it.message)
827                })
828            })
829            .or_else(|| as_text_of(&envelope.message))
830            .or_else(|| as_text_of(&envelope.detail))
831            .or_else(|| {
832                detail.as_ref().and_then(|detail| detail.message.as_ref()).and_then(as_text)
833            })
834            .or_else(|| joined_detail_list(&envelope.detail))
835            .filter(|message| !message.is_empty());
836
837        Self {
838            message: message.map_or_else(|| json_message(body), |message| bounded(&message)),
839            error_type,
840        }
841    }
842}
843
844/// The value as a JSON string, or nothing when it is any other shape.
845fn as_text(raw: &RawJson) -> Option<String> {
846    raw.decode::<String>().ok()
847}
848
849/// The same, for a member that may be absent.
850fn as_text_of(raw: &Option<RawJson>) -> Option<String> {
851    raw.as_ref().and_then(as_text)
852}
853
854/// A string reached by descending one level into a member.
855fn member_text(
856    raw: &Option<RawJson>,
857    member: impl FnOnce(&RawJson) -> Option<RawJson>,
858) -> Option<String> {
859    raw.as_ref().and_then(member).as_ref().and_then(as_text)
860}
861
862/// A list under `detail` rendered as one sentence.
863///
864/// Entries that are not objects, and objects whose `msg` is not a string, are
865/// dropped; an entry's `loc` becomes a dotted prefix with the segment `body`
866/// left out, because it names the request part rather than the field.
867fn joined_detail_list(raw: &Option<RawJson>) -> Option<String> {
868    let entries = raw.as_ref()?.decode::<Vec<RawJson>>().ok()?;
869    let mut parts = Vec::with_capacity(entries.len());
870    for entry in &entries {
871        let Ok(entry) = entry.decode::<DetailEntry>() else {
872            continue;
873        };
874        let Some(message) = entry.msg.as_ref().and_then(as_text) else {
875            continue;
876        };
877        let path = entry.loc.as_ref().map(location_path).unwrap_or_default();
878        parts.push(if path.is_empty() { message } else { format!("{path}: {message}") });
879    }
880    if parts.is_empty() { None } else { Some(parts.join("; ")) }
881}
882
883/// The dotted form of a validation framework's `loc` list.
884fn location_path(raw: &RawJson) -> String {
885    let Ok(segments) = raw.decode::<Vec<RawJson>>() else {
886        return String::new();
887    };
888    let mut path = String::new();
889    for segment in &segments {
890        // A segment is a field name or an index into a list. Anything else is
891        // left out rather than guessed at.
892        let rendered = match segment.decode::<String>() {
893            Ok(name) if name == "body" => continue,
894            Ok(name) => name,
895            Err(_) => match segment.decode::<i64>() {
896                Ok(index) => index.to_string(),
897                Err(_) => continue,
898            },
899        };
900        if !path.is_empty() {
901            path.push('.');
902        }
903        path.push_str(&rendered);
904    }
905    path
906}
907
908/// A JSON body used as its own message, for a body that says nothing this
909/// crate recognizes.
910///
911/// The text is compacted, so a pretty-printed body does not put newlines into
912/// a one-line message, and bounded like every message read from a body.
913fn json_message(body: &[u8]) -> Box<str> {
914    let text = String::from_utf8_lossy(body);
915    bounded(&compact(&text))
916}
917
918/// A body that is not JSON at all, used as its own message.
919///
920/// Nothing is compacted here: the bytes are not JSON, so whitespace between
921/// them is not punctuation and dropping it would change what the server said.
922/// A line break or any other control character among them is escaped instead.
923fn text_message(body: &[u8]) -> Box<str> {
924    bounded(&String::from_utf8_lossy(body))
925}
926
927/// A body no member could be read out of because it did not parse.
928///
929/// A body the depth guard turned away is JSON all the same - it is only more
930/// deeply nested than this crate will walk - so it is compacted like any other
931/// JSON body. Anything else did not parse because it is not JSON, and its
932/// whitespace is kept.
933fn unparsed_message(body: &[u8], failure: &DecodeError) -> Box<str> {
934    if failure.kind() == DecodeErrorKind::TooDeep { json_message(body) } else { text_message(body) }
935}
936
937/// The server's `text` as a message holds it: escaped and cut at
938/// [`text::MAX_MESSAGE_CHARS`] characters as `crate::text` describes, with a
939/// backslash kept as it is.
940///
941/// The backslash is kept because a JSON body standing in for a message already
942/// spells its escapes with one (`"a\nb"`), and a second pass would turn every
943/// one of them into `\\n`; what the message loses in exactness `body_text`
944/// keeps.
945fn bounded(text: &str) -> Box<str> {
946    text::bounded(&text, text::MAX_MESSAGE_CHARS).into_boxed_str()
947}
948
949/// The first byte of `body` that is not JSON whitespace.
950fn first_token(body: &[u8]) -> Option<u8> {
951    body.iter().copied().find(|byte| !byte.is_ascii_whitespace())
952}
953
954/// Drops the whitespace between JSON tokens, leaving the whitespace inside
955/// strings alone.
956///
957/// The text is not re-encoded: numbers, escapes and key order come out exactly
958/// as the server wrote them, so the message shows what arrived rather than
959/// what a round trip through this crate's codec would have made of it. Text
960/// that is not JSON comes back unchanged, because nothing in it is outside a
961/// string that a JSON reader would recognize.
962fn compact(text: &str) -> Cow<'_, str> {
963    let mut in_string = false;
964    let mut escaped = false;
965    // Stays `None` while nothing has been dropped, which is what lets a body
966    // that is already compact come back borrowed.
967    let mut kept: Option<String> = None;
968    let mut copied = 0;
969    for (at, byte) in text.bytes().enumerate() {
970        if in_string {
971            match byte {
972                _ if escaped => escaped = false,
973                b'\\' => escaped = true,
974                b'"' => in_string = false,
975                _ => {}
976            }
977            continue;
978        }
979        match byte {
980            b'"' => in_string = true,
981            b' ' | b'\t' | b'\n' | b'\r' => {
982                let kept = kept.get_or_insert_with(|| String::with_capacity(text.len()));
983                kept.push_str(&text[copied..at]);
984                copied = at + 1;
985            }
986            _ => {}
987        }
988    }
989    match kept {
990        Some(mut kept) => {
991            kept.push_str(&text[copied..]);
992            Cow::Owned(kept)
993        }
994        None => Cow::Borrowed(text),
995    }
996}
997
998#[cfg(test)]
999#[path = "error_tests.rs"]
1000mod tests;