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;