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;