tuwunel_core/error/mod.rs
1//! Defines shared error and result types.
2//!
3//! Errors retain protocol and transport context across crate boundaries. The
4//! module also provides the macros used to construct and report them.
5
6mod err;
7mod log;
8mod panic;
9mod response;
10mod serde;
11#[cfg(test)]
12mod tests;
13
14use std::{
15 any::Any,
16 borrow::Cow,
17 convert::Infallible,
18 sync::{Mutex, PoisonError},
19};
20
21pub use self::{err::visit, log::*};
22use crate::utils::{assert_ref_unwind_safe, assert_send, assert_sync, assert_unwind_safe};
23
24/// Unifies failures raised by the core crate.
25///
26/// Variants preserve typed causes where available and carry contextual text for
27/// domain-specific failures. Conversion implementations allow callers to
28/// propagate common dependency errors with `?`.
29#[derive(thiserror::Error)]
30pub enum Error {
31 /// Carries an arbitrary panic payload.
32 ///
33 /// The payload is protected by a mutex so the error remains shareable
34 /// across unwind boundaries. Use the panic helpers to resume unwinding or
35 /// inspect it.
36 #[error("PANIC!")]
37 PanicAny(Mutex<Box<dyn Any + Send>>),
38
39 /// Carries a panic payload and its extracted static message.
40 ///
41 /// The message supports diagnostics without consuming the payload. The
42 /// mutex keeps the payload available across unwind boundaries.
43 #[error("PANIC! {0}")]
44 Panic(&'static str, Mutex<Box<dyn Any + Send + 'static>>),
45
46 // std
47 /// Reports a formatting failure.
48 ///
49 /// Automatic conversion preserves the original source error. Its display
50 /// text is forwarded unchanged.
51 #[error(transparent)]
52 Fmt(#[from] std::fmt::Error),
53
54 /// Reports an invalid UTF-8 byte sequence while constructing a string.
55 ///
56 /// Automatic conversion preserves the original bytes and source error. Its
57 /// display text is forwarded unchanged.
58 #[error(transparent)]
59 FromUtf8(#[from] std::string::FromUtf8Error),
60
61 /// Reports an input or output failure.
62 ///
63 /// Automatic conversion preserves the original I/O error and its kind. HTTP
64 /// response mapping may use the contained error kind.
65 #[error("I/O error: {0}")]
66 Io(#[from] std::io::Error),
67
68 /// Reports a floating-point parsing failure.
69 ///
70 /// Automatic conversion preserves the original source error. Its display
71 /// text is forwarded unchanged.
72 #[error(transparent)]
73 ParseFloat(#[from] std::num::ParseFloatError),
74
75 /// Reports an integer parsing failure.
76 ///
77 /// Automatic conversion preserves the original source error. Its display
78 /// text is forwarded unchanged.
79 #[error(transparent)]
80 ParseInt(#[from] std::num::ParseIntError),
81
82 /// Carries a dynamically typed standard error.
83 ///
84 /// The boxed source must be safe to send and share between threads. Its
85 /// display text and source chain remain available for diagnostics.
86 #[error(transparent)]
87 Std(#[from] Box<dyn std::error::Error + Send + Sync + 'static>),
88
89 /// Reports a system clock value earlier than the requested reference time.
90 ///
91 /// Automatic conversion preserves the original source error. Its display
92 /// text is forwarded unchanged.
93 #[error(transparent)]
94 SystemTime(#[from] std::time::SystemTimeError),
95
96 /// Reports failure to access thread-local state.
97 ///
98 /// Automatic conversion preserves the original source error. Its display
99 /// text is forwarded unchanged.
100 #[error(transparent)]
101 ThreadAccessError(#[from] std::thread::AccessError),
102
103 /// Reports an integer conversion outside the destination range.
104 ///
105 /// Automatic conversion preserves the original source error. Its display
106 /// text is forwarded unchanged.
107 #[error(transparent)]
108 TryFromInt(#[from] std::num::TryFromIntError),
109
110 /// Reports conversion from a slice with an incompatible length.
111 ///
112 /// Automatic conversion preserves the original source error. Its display
113 /// text is forwarded unchanged.
114 #[error(transparent)]
115 TryFromSlice(#[from] std::array::TryFromSliceError),
116
117 /// Reports an invalid borrowed UTF-8 byte sequence.
118 ///
119 /// Automatic conversion preserves the original source error. Its display
120 /// text is forwarded unchanged.
121 #[error(transparent)]
122 Utf8(#[from] std::str::Utf8Error),
123
124 // third-party
125 /// Reports that a fixed-capacity collection cannot accept another item.
126 ///
127 /// Automatic conversion preserves the original capacity error. Its display
128 /// text is forwarded unchanged.
129 #[error(transparent)]
130 CapacityError(#[from] arrayvec::CapacityError),
131
132 /// Reports failure to parse Cargo manifest data.
133 ///
134 /// Automatic conversion preserves the original source error. Its display
135 /// text is forwarded unchanged.
136 #[error(transparent)]
137 CargoToml(#[from] cargo_toml::Error),
138
139 /// Reports a command-line parsing or presentation failure.
140 ///
141 /// Automatic conversion preserves the original Clap error. Its display text
142 /// is forwarded unchanged.
143 #[error(transparent)]
144 Clap(#[from] clap::error::Error),
145
146 /// Reports a Unix system error number.
147 ///
148 /// Automatic conversion preserves the original error number. Its display
149 /// text is forwarded unchanged.
150 #[cfg(unix)]
151 #[error(transparent)]
152 Errno(#[from] nix::errno::Errno),
153
154 /// Reports rejection of a required Axum request extension.
155 ///
156 /// Automatic conversion preserves the extractor rejection. Its display text
157 /// is forwarded unchanged.
158 #[error(transparent)]
159 Extension(#[from] axum::extract::rejection::ExtensionRejection),
160
161 /// Reports a configuration extraction failure.
162 ///
163 /// The boxed Figment error preserves its complete source and diagnostic
164 /// context. A dedicated conversion boxes it for ordinary `?` propagation.
165 #[error(transparent)]
166 Figment(Box<figment::error::Error>),
167
168 /// Reports failure to deserialize an HTML form.
169 ///
170 /// Automatic conversion preserves the original source error. Its display
171 /// text is forwarded unchanged.
172 #[error(transparent)]
173 HtmlFormDe(#[from] serde_html_form::de::Error),
174
175 /// Reports failure to serialize an HTML form.
176 ///
177 /// Automatic conversion preserves the original source error. Its display
178 /// text is forwarded unchanged.
179 #[error(transparent)]
180 HtmlFormSer(#[from] serde_html_form::ser::Error),
181
182 /// Reports failure to construct an HTTP value.
183 ///
184 /// Automatic conversion preserves the original source error. Its display
185 /// text is forwarded unchanged.
186 #[error(transparent)]
187 Http(#[from] http::Error),
188
189 /// Reports an invalid HTTP header value.
190 ///
191 /// Automatic conversion preserves the original source error. Its display
192 /// text is forwarded unchanged.
193 #[error(transparent)]
194 HttpHeader(#[from] http::header::InvalidHeaderValue),
195
196 /// Reports failure of a spawned asynchronous task.
197 ///
198 /// The Tokio join error records cancellation and panic state. Panic helpers
199 /// can recover its payload when the task panicked.
200 #[error("Join error: {0}")]
201 JoinError(#[from] tokio::task::JoinError),
202
203 /// Reports failure to serialize or deserialize JSON.
204 ///
205 /// Automatic conversion preserves the original source error. Matrix error
206 /// mapping classifies this variant as invalid JSON.
207 #[error(transparent)]
208 Json(#[from] serde_json::Error),
209
210 /// Reports failure to parse a Matrix-compatible JavaScript integer.
211 ///
212 /// Automatic conversion preserves the re-exported integer error. Matrix
213 /// response mapping treats it as a bad request.
214 #[error(transparent)]
215 JsParseInt(#[from] ruma::JsParseIntError), // js_int re-export
216
217 /// Reports a value outside the Matrix JavaScript-integer range.
218 ///
219 /// Automatic conversion preserves the re-exported conversion error. Matrix
220 /// response mapping treats it as a bad request.
221 #[error(transparent)]
222 JsTryFromInt(#[from] ruma::JsTryFromIntError), // js_int re-export
223
224 /// Reports an object-storage operation failure.
225 ///
226 /// Automatic conversion preserves the backend source error. Its display
227 /// text is forwarded unchanged.
228 #[error(transparent)]
229 ObjectStore(#[from] object_store::Error),
230
231 /// Reports rejection of an Axum path parameter.
232 ///
233 /// Automatic conversion preserves the extractor rejection. Its display text
234 /// is forwarded unchanged.
235 #[error(transparent)]
236 Path(#[from] axum::extract::rejection::PathRejection),
237
238 /// Reports access to a poisoned synchronization primitive.
239 ///
240 /// The stored text reports the poisoning without retaining the guard.
241 /// Poison conversion supplies the originating error text.
242 #[error("Mutex poisoned: {0}")]
243 Poison(Cow<'static, str>),
244
245 /// Reports an invalid regular expression.
246 ///
247 /// Automatic conversion preserves the original source error. The formatted
248 /// message identifies the regex failure.
249 #[error("Regex error: {0}")]
250 Regex(#[from] regex::Error),
251
252 /// Reports an HTTP client request failure.
253 ///
254 /// Automatic conversion preserves status and transport details from
255 /// Reqwest. HTTP response mapping reuses its status when one is available.
256 #[error("Request error: {0}")]
257 Reqwest(#[from] reqwest::Error),
258
259 /// Reports a custom deserialization failure.
260 ///
261 /// The message may borrow static text or own formatted context. It is
262 /// exposed directly as the error display.
263 #[error("{0}")]
264 SerdeDe(Cow<'static, str>),
265
266 /// Reports a custom serialization failure.
267 ///
268 /// The message may borrow static text or own formatted context. It is
269 /// exposed directly as the error display.
270 #[error("{0}")]
271 SerdeSer(Cow<'static, str>),
272
273 /// Reports failure to deserialize TOML.
274 ///
275 /// Automatic conversion preserves the original source error. Its display
276 /// text is forwarded unchanged.
277 #[error(transparent)]
278 TomlDe(#[from] toml::de::Error),
279
280 /// Reports failure to serialize TOML.
281 ///
282 /// Automatic conversion preserves the original source error. Its display
283 /// text is forwarded unchanged.
284 #[error(transparent)]
285 TomlSer(#[from] toml::ser::Error),
286
287 /// Reports an invalid tracing filter directive.
288 ///
289 /// Automatic conversion preserves the filter parser's source error. The
290 /// formatted message identifies the tracing subsystem.
291 #[error("Tracing filter error: {0}")]
292 TracingFilter(#[from] tracing_subscriber::filter::ParseError),
293
294 /// Reports failure to reload a tracing layer.
295 ///
296 /// Automatic conversion preserves the reload source error. The formatted
297 /// message identifies the tracing subsystem.
298 #[error("Tracing reload error: {0}")]
299 TracingReload(#[from] tracing_subscriber::reload::Error),
300
301 /// Reports rejection of a typed HTTP header.
302 ///
303 /// Automatic conversion preserves the extractor rejection. Its display text
304 /// is forwarded unchanged.
305 #[error(transparent)]
306 TypedHeader(#[from] axum_extra::typed_header::TypedHeaderRejection),
307
308 /// Reports failure to parse a URL.
309 ///
310 /// Automatic conversion preserves the original source error. Its display
311 /// text is forwarded unchanged.
312 #[error(transparent)]
313 UrlParse(#[from] url::ParseError),
314
315 /// Reports failure to serialize or deserialize YAML.
316 ///
317 /// Automatic conversion preserves the original source error. Its display
318 /// text is forwarded unchanged.
319 #[error(transparent)]
320 Yaml(#[from] serde_yaml::Error),
321
322 // ruma/tuwunel
323 /// Reports an arithmetic operation that cannot produce a valid result.
324 ///
325 /// The message records contextual conversion, range, overflow, and
326 /// underflow failures. It can supplement lower-level typed numeric source
327 /// errors.
328 #[error("Arithmetic operation failed: {0}")]
329 Arithmetic(Cow<'static, str>),
330
331 /// State-res `auth_check` rejection sentinel.
332 ///
333 /// Surfaces to the wire as 403 / M_FORBIDDEN with the Display text
334 /// `Auth check failed: {inner}`. Exists so callers can pattern-match the
335 /// cause without grepping the message text.
336 #[error("Auth check failed: {0}")]
337 AuthCheck(Box<Self>),
338
339 /// Reports a legacy structured Matrix request error.
340 ///
341 /// The variant pairs a Matrix error kind with static public text. Response
342 /// mapping derives the appropriate HTTP status from that kind.
343 #[error("{0}: {1}")]
344 BadRequest(ruma::api::error::ErrorKind, &'static str), //TODO: remove
345
346 /// Reports an invalid or unusable response from a remote server.
347 ///
348 /// The message carries protocol context suitable for diagnostics. No typed
349 /// remote response is retained.
350 #[error("{0}")]
351 BadServerResponse(Cow<'static, str>),
352
353 /// Reports invalid canonical JSON.
354 ///
355 /// Automatic conversion preserves the original canonicalization error.
356 /// Matrix error mapping classifies this variant as invalid JSON.
357 #[error(transparent)]
358 CanonicalJson(#[from] ruma::CanonicalJsonError),
359
360 /// Reports an invalid configuration directive.
361 ///
362 /// The static directive name identifies the setting and the accompanying
363 /// message explains why its value cannot be used.
364 #[error("There was a problem with the '{0}' directive in your configuration: {1}")]
365 Config(&'static str, Cow<'static, str>),
366
367 /// Reports a resource conflict.
368 ///
369 /// This variant currently represents an already occupied room alias. HTTP
370 /// response mapping emits a conflict status.
371 #[error("{0}")]
372 Conflict(Cow<'static, str>), // This is only needed for when a room alias already exists
373
374 /// Reports an invalid Matrix content-disposition header.
375 ///
376 /// Automatic conversion preserves the original parser error. Its display
377 /// text is forwarded unchanged.
378 #[error(transparent)]
379 ContentDisposition(#[from] ruma::http_headers::ContentDispositionParseError),
380
381 /// Reports a database operation or invariant failure.
382 ///
383 /// The message carries storage context without exposing it in sanitized
384 /// client-facing output. Database failures default to an internal status.
385 #[error("{0}")]
386 Database(Cow<'static, str>),
387
388 /// Reports use of a feature disabled by server configuration.
389 ///
390 /// The stored feature name is included in the public message. Matrix
391 /// response mapping assigns the matching feature-disabled error kind.
392 #[error("Feature '{0}' is not available on this server.")]
393 FeatureDisabled(Cow<'static, str>),
394
395 /// Reports an error response received from a federated server.
396 ///
397 /// The variant preserves both the origin and its structured Matrix error,
398 /// so internal callers can dispatch on what the remote said. Response
399 /// mapping does not forward that status and kind unconditionally.
400 #[error("Remote server {0} responded with: {1}")]
401 Federation(ruma::OwnedServerName, ruma::api::error::Error),
402
403 /// Carries a preconstructed HTTP status and JSON response body.
404 ///
405 /// Response mapping preserves the caller-selected status. The formatted
406 /// JSON becomes the message of a standard Matrix error response.
407 #[error("{0}: {1:#?}")]
408 HttpJson(http::StatusCode, axum::Json<serde_json::Value>),
409
410 /// Reports an invariant violation in a room's persisted state.
411 ///
412 /// The static message names the failed invariant and the room identifier
413 /// locates the affected state.
414 #[error("{0} in {1}")]
415 InconsistentRoomState(&'static str, ruma::OwnedRoomId),
416
417 /// Reports failure to convert a Matrix response into HTTP form.
418 ///
419 /// Automatic conversion preserves the original Ruma source error. Its
420 /// display text is forwarded unchanged.
421 #[error(transparent)]
422 IntoHttp(#[from] ruma::api::error::IntoHttpError),
423
424 /// Reports an LDAP operation failure.
425 ///
426 /// The message records directory-service context not represented by a
427 /// common typed source. Response mapping treats it as an internal error.
428 #[error("{0}")]
429 Ldap(Cow<'static, str>),
430
431 /// Reports an invalid Matrix content URI.
432 ///
433 /// Automatic conversion preserves the original URI error. Its display text
434 /// is forwarded unchanged.
435 #[error(transparent)]
436 Mxc(#[from] ruma::MxcUriError),
437
438 /// Reports an invalid Matrix identifier.
439 ///
440 /// Automatic conversion preserves the original identifier parser error. Its
441 /// display text is forwarded unchanged.
442 #[error(transparent)]
443 Mxid(#[from] ruma::IdParseError),
444
445 /// Reports invalid room power-level content or arithmetic.
446 ///
447 /// Automatic conversion preserves the original Ruma power-level error. Its
448 /// display text is forwarded unchanged.
449 #[error(transparent)]
450 PowerLevels(#[from] ruma::events::room::power_levels::PowerLevelsError),
451
452 /// Reports failure to redact canonical JSON from a remote server.
453 ///
454 /// The variant records the origin alongside the invalid canonical field.
455 /// The formatted message keeps both pieces of context.
456 #[error("from {0}: {1}")]
457 Redaction(ruma::OwnedServerName, ruma::canonical_json::CanonicalJsonFieldError),
458
459 /// Carries a structured Matrix client error response.
460 ///
461 /// The variant stores the Matrix error kind, public message, and preferred
462 /// HTTP status. Response mapping may refine the status from the kind.
463 #[error("{0}: {1}")]
464 Request(ruma::api::error::ErrorKind, Cow<'static, str>, http::StatusCode),
465
466 /// Reports a structured Matrix API error.
467 ///
468 /// Automatic conversion preserves the Ruma status, kind, and message.
469 /// Response mapping forwards those structured fields.
470 #[error(transparent)]
471 Ruma(#[from] ruma::api::error::Error),
472
473 /// Reports a Matrix signature verification failure.
474 ///
475 /// Automatic conversion preserves the original verification error. Its
476 /// display text is forwarded unchanged.
477 #[error(transparent)]
478 Signatures(#[from] ruma::signatures::VerificationError),
479
480 /// Reports invalid JSON encountered during signature processing.
481 ///
482 /// Automatic conversion preserves the signature library's JSON error. Its
483 /// display text is forwarded unchanged.
484 #[error(transparent)]
485 SignaturesJson(#[from] ruma::signatures::JsonError),
486
487 /// Requests an interactive-authentication challenge response.
488 ///
489 /// The contained UIAA information is serialized for the client rather than
490 /// treated as an opaque internal failure.
491 #[error("uiaa")]
492 Uiaa(ruma::api::client::uiaa::UiaaInfo),
493
494 // unique / untyped
495 /// Reports an untyped core failure.
496 ///
497 /// The message may borrow static text or own formatted context. This
498 /// fallback is used when no structured variant represents the failure.
499 #[error("{0}")]
500 Err(Cow<'static, str>),
501}
502
503static _IS_SEND: () = assert_send::<Error>();
504static _IS_SYNC: () = assert_sync::<Error>();
505static _IS_UNWIND_SAFE: () = assert_unwind_safe::<Error>();
506static _IS_REF_UNWIND_SAFE: () = assert_ref_unwind_safe::<Error>();
507
508impl Error {
509 /// Captures the operating system's most recent error for the current
510 /// thread.
511 ///
512 /// The error is sampled when this function is called and wrapped as
513 /// [`Error::Io`]. Platform-specific code remains available through the
514 /// source.
515 #[inline]
516 #[must_use]
517 pub fn from_errno() -> Self { Self::Io(std::io::Error::last_os_error()) }
518
519 /// Constructs a database error from static diagnostic text.
520 ///
521 /// The error helper records the call site while preserving the supplied
522 /// message. Callers exposing it publicly can use
523 /// [`Error::sanitized_message`].
524 //#[deprecated]
525 pub fn bad_database(message: &'static str) -> Self {
526 crate::err!(Database(error!("{message}")))
527 }
528
529 /// Produces an error message safe for public responses.
530 ///
531 /// Database and I/O details are replaced with generic text to avoid leaking
532 /// sensitive context. Other variants retain their normal message.
533 pub fn sanitized_message(&self) -> String {
534 match self {
535 | Self::Database(..) => String::from("Database error occurred."),
536 | Self::Io(..) => String::from("I/O error occurred."),
537 | _ => self.message(),
538 }
539 }
540
541 /// Formats the diagnostic message for this error.
542 ///
543 /// Federation errors include their origin and Ruma errors use their Matrix
544 /// response message. Other variants use their
545 /// [`Display`](std::fmt::Display) implementation.
546 pub fn message(&self) -> String {
547 match self {
548 | Self::Federation(origin, error) => format!("Answer from {origin}: {error}"),
549 | Self::Ruma(error) => response::ruma_error_message(error),
550 | _ => format!("{self}"),
551 }
552 }
553
554 /// Returns the Matrix error kind represented by this error.
555 ///
556 /// Structured request and federation variants preserve their supplied kind.
557 /// Unclassified internal errors map to `M_UNKNOWN`.
558 #[inline]
559 pub fn kind(&self) -> ruma::api::error::ErrorKind {
560 use ruma::api::error::{
561 ErrorKind,
562 ErrorKind::{FeatureDisabled, NotJson, Unknown},
563 };
564
565 match self {
566 | Self::FeatureDisabled(..) => FeatureDisabled,
567 | Self::CanonicalJson(..) | Self::Json(..) => NotJson,
568 | Self::AuthCheck(..) => ErrorKind::forbidden(),
569 | Self::BadRequest(kind, ..) | Self::Request(kind, ..) => kind.clone(),
570 | Self::Federation(_, error) | Self::Ruma(error) =>
571 response::ruma_error_kind(error).clone(),
572 | _ => Unknown,
573 }
574 }
575
576 /// Returns the HTTP status represented by this error.
577 ///
578 /// Structured variants preserve or derive their protocol status, while I/O
579 /// and client errors use their available status metadata. Unclassified
580 /// failures map to an internal-server-error status.
581 pub fn status_code(&self) -> http::StatusCode {
582 use http::StatusCode;
583
584 match self {
585 | Self::AuthCheck(..) => StatusCode::FORBIDDEN,
586 | Self::Conflict(_) => StatusCode::CONFLICT, // room alias exists
587 | Self::Federation(_, error) | Self::Ruma(error) => error.status_code,
588 | Self::FeatureDisabled(..)
589 | Self::CanonicalJson(..)
590 | Self::Json(..)
591 | Self::JsParseInt(..)
592 | Self::JsTryFromInt(..) => response::bad_request_code(&self.kind()),
593 | Self::BadRequest(kind, ..) => response::bad_request_code(kind),
594 | Self::Request(kind, _, code) => response::status_code(kind, *code),
595 | Self::Io(error) => response::io_error_code(error.kind()),
596 | Self::HttpJson(code, ..) => *code,
597 | Self::Reqwest(error) => error
598 .status()
599 .unwrap_or(StatusCode::INTERNAL_SERVER_ERROR),
600 | _ => StatusCode::INTERNAL_SERVER_ERROR,
601 }
602 }
603
604 /// Tests whether this error maps to an HTTP not-found status.
605 ///
606 /// The test includes contained error types whose status mapping yields 404.
607 /// Callers can use it to treat `Err` as the absent case in place of a
608 /// nested `Option`.
609 #[inline]
610 pub fn is_not_found(&self) -> bool { self.status_code() == http::StatusCode::NOT_FOUND }
611}
612
613impl std::fmt::Debug for Error {
614 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
615 write!(f, "{}", self.message())
616 }
617}
618
619impl<T> From<PoisonError<T>> for Error {
620 #[cold]
621 #[inline(never)]
622 fn from(e: PoisonError<T>) -> Self { Self::Poison(e.to_string().into()) }
623}
624
625impl From<figment::error::Error> for Error {
626 #[cold]
627 #[inline(never)]
628 fn from(e: figment::error::Error) -> Self { Self::Figment(Box::new(e)) }
629}
630
631#[expect(clippy::fallible_impl_from)]
632impl From<Infallible> for Error {
633 #[cold]
634 #[inline(never)]
635 fn from(_e: Infallible) -> Self {
636 panic!("infallible error should never exist");
637 }
638}
639
640/// Marks an impossible [`Infallible`] error path.
641///
642/// The argument cannot be constructed in safe code, so reaching this function
643/// indicates a violated invariant.
644///
645/// # Panics
646///
647/// Always panics because an `Infallible` error cannot legitimately exist.
648#[cold]
649#[inline(never)]
650pub fn infallible(_e: &Infallible) {
651 panic!("infallible error should never exist");
652}
653
654/// Produces a public-safe message from an owned error.
655///
656/// Its by-value signature adapts [`Error::sanitized_message`] for iterator and
657/// future combinators that consume their item. Sanitization behavior is
658/// identical to the method.
659#[inline]
660#[must_use]
661#[expect(clippy::needless_pass_by_value)]
662pub fn sanitized_message(e: Error) -> String { e.sanitized_message() }