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