typesafe_sdk/transport/mod.rs
1//! The seam between this crate and whatever sends the bytes.
2//!
3//! The SDK drives a `tower`-shaped service rather than an HTTP client, so a
4//! caller can put their own stack underneath it - a recorder, a proxy, an
5//! in-memory harness - without this crate knowing about it, and the default
6//! stack, [`HyperTransport`], is one implementation of that seam rather than a
7//! hard dependency. Any [`tower_service::Service`] over `http` requests with
8//! this crate's [`Body`] is a transport; [`HttpService`] names the bounds.
9//!
10//! The request body is this crate's own type implementing `http-body`, not a
11//! type borrowed from a pre-1.0 crate, so the public signature does not commit
12//! a caller to a version of somebody else's dependency.
13//!
14//! A transport sends one request. Everything around that - the headers every
15//! request carries, the deadline, reading the response under the size limit,
16//! turning a failure into this crate's [`Error`] - happens here, once per
17//! attempt, whichever transport is underneath.
18
19mod hyper;
20
21use std::{
22 convert::Infallible,
23 error::Error as StdError,
24 fmt,
25 future::{Future, poll_fn},
26 pin::Pin,
27 task::{Context, Poll},
28 time::Duration,
29};
30
31use bytes::Bytes;
32use http::{
33 HeaderMap, HeaderName, HeaderValue, Method, Request, Response, StatusCode, Uri,
34 header::CONTENT_TYPE,
35};
36use http_body::{Frame, SizeHint};
37use http_body_util::{BodyExt as _, LengthLimitError, Limited};
38use tower_service::Service;
39
40pub(crate) use self::hyper::TransportSettings;
41pub use self::hyper::{HttpVersion, HyperResponseFuture, HyperTransport, ResponseBody};
42use crate::{
43 config::Config,
44 constants::{
45 JSON_CONTENT_TYPE, PROTECTED_HEADERS, RETRY_COUNT_HEADER, RUNTIME_IDENTIFIER,
46 SDK_IDENTIFIER, TRANSPORT_HEADERS,
47 },
48 error::{ApiError, Error, ErrorKind, format_endpoint},
49 question::upsert,
50 redact::{self, Credentials, Outcome},
51 telemetry,
52 text::{self, Backslash, SafeText},
53};
54
55/// The error type a transport may fail with: any error that can cross
56/// threads.
57pub type BoxError = Box<dyn StdError + Send + Sync>;
58
59// ------------------------------------------------------------------- Body
60
61/// The body of a request the SDK sends: finished bytes, handed over in one
62/// frame.
63///
64/// Its length is known before the first byte is sent, so `size_hint` is exact
65/// and a transport can write a `Content-Length` without buffering. Cloning it
66/// shares the bytes rather than copying them.
67///
68/// `Debug` prints the length only: a body carries the caller's `state`, which
69/// may be personal data.
70#[derive(Clone, Default)]
71pub struct Body {
72 /// `None` once the one frame has been handed out, or for an empty body.
73 data: Option<Bytes>,
74}
75
76impl Body {
77 /// A body with no bytes, as a `GET` carries.
78 #[must_use]
79 pub fn empty() -> Self {
80 Self { data: None }
81 }
82
83 /// The bytes not yet handed out as a frame.
84 #[must_use]
85 pub fn len(&self) -> usize {
86 self.data.as_ref().map_or(0, Bytes::len)
87 }
88
89 /// Whether no bytes are left to hand out.
90 #[must_use]
91 pub fn is_empty(&self) -> bool {
92 self.len() == 0
93 }
94}
95
96impl From<Bytes> for Body {
97 fn from(bytes: Bytes) -> Self {
98 // An empty buffer is no frame at all, so that `is_end_stream` is true
99 // from the start and a transport sends no empty DATA frame.
100 Self { data: (!bytes.is_empty()).then_some(bytes) }
101 }
102}
103
104impl fmt::Debug for Body {
105 fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
106 formatter.debug_struct("Body").field("len", &self.len()).finish()
107 }
108}
109
110impl http_body::Body for Body {
111 type Data = Bytes;
112 type Error = Infallible;
113
114 fn poll_frame(
115 self: Pin<&mut Self>,
116 _: &mut Context<'_>,
117 ) -> Poll<Option<Result<Frame<Bytes>, Infallible>>> {
118 // `Body` holds nothing that cares where it lives in memory, so the
119 // pinned reference can be turned back into a plain one.
120 Poll::Ready(self.get_mut().data.take().map(|bytes| Ok(Frame::data(bytes))))
121 }
122
123 fn is_end_stream(&self) -> bool {
124 self.data.is_none()
125 }
126
127 fn size_hint(&self) -> SizeHint {
128 SizeHint::with_exact(self.len() as u64)
129 }
130}
131
132// ------------------------------------------------------------ HttpService
133
134mod sealed {
135 /// Keeps [`HttpService`](super::HttpService) implemented only through its
136 /// blanket implementation.
137 pub trait Sealed {}
138}
139
140/// What a transport has to be: a `tower` service that takes an `http` request
141/// with this crate's [`Body`] and answers with an `http` response.
142///
143/// It is implemented for every [`tower_service::Service`] that fits, and for
144/// nothing else, so it is a name for a set of bounds rather than a trait to
145/// implement: implement `Service` and a type is a transport. The service is
146/// cloned for every request and driven with its own `poll_ready`, so a
147/// service with back-pressure keeps it; the default [`HyperTransport`] is
148/// always ready.
149///
150/// The error of the service, and of its response body, is anything that can
151/// cross threads. It is kept as the [`source`](StdError::source) of the
152/// connection [`Error`] the call fails with.
153pub trait HttpService: sealed::Sealed + Clone + Send + Sync + 'static {
154 /// The body of the responses the service answers with.
155 type ResponseBody: http_body::Body<Data: Send, Error: Into<BoxError>> + Send + 'static;
156 /// What the service fails with when it cannot produce a response.
157 type Error: Into<BoxError>;
158 /// The future a call returns.
159 type Future: Future<Output = Result<Response<Self::ResponseBody>, Self::Error>> + Send;
160
161 /// [`Service::poll_ready`], forwarded.
162 ///
163 /// # Errors
164 ///
165 /// Returns the service's error when it can take no more requests.
166 fn poll_ready(&mut self, cx: &mut Context<'_>) -> Poll<Result<(), Self::Error>>;
167
168 /// [`Service::call`], forwarded.
169 fn call(&mut self, request: Request<Body>) -> Self::Future;
170}
171
172impl<S, B> sealed::Sealed for S
173where
174 S: Service<Request<Body>, Response = Response<B>> + Clone + Send + Sync + 'static,
175 S::Error: Into<BoxError>,
176 S::Future: Send,
177 B: http_body::Body<Data: Send, Error: Into<BoxError>> + Send + 'static,
178{
179}
180
181impl<S, B> HttpService for S
182where
183 S: Service<Request<Body>, Response = Response<B>> + Clone + Send + Sync + 'static,
184 S::Error: Into<BoxError>,
185 S::Future: Send,
186 B: http_body::Body<Data: Send, Error: Into<BoxError>> + Send + 'static,
187{
188 type ResponseBody = B;
189 type Error = S::Error;
190 type Future = S::Future;
191
192 fn poll_ready(&mut self, cx: &mut Context<'_>) -> Poll<Result<(), S::Error>> {
193 Service::poll_ready(self, cx)
194 }
195
196 fn call(&mut self, request: Request<Body>) -> S::Future {
197 Service::call(self, request)
198 }
199}
200
201// ------------------------------------------------------- header assembly
202
203/// The headers every request of one kind carries, built once per client.
204///
205/// Precedence, lowest first: the client's default headers, then the SDK's
206/// own, which no default can replace, and, when the request has a body,
207/// `Content-Type: application/json`. A default `X-TypeSafe-Retry-Count` is
208/// dropped: the SDK sets that header on retries and only there. So is a
209/// default framing or connection header ([`TRANSPORT_HEADERS`]). Per-call
210/// headers are applied on top of this map for each attempt; see
211/// [`call_headers`]. `User-Agent` carries the value the configuration built
212/// once, and `X-TypeSafe-Runtime` is left out when the configuration says so;
213/// a caller's header of either name is dropped all the same.
214pub(crate) fn base_headers(config: &Config, with_body: bool) -> HeaderMap {
215 let defaults = config.default_headers();
216 // Room for every default, the protected five and `Content-Type`, so
217 // building the map never grows it.
218 let mut headers = HeaderMap::with_capacity(defaults.len() + PROTECTED_HEADERS.len() + 1);
219 for (name, value) in defaults {
220 if !is_sdk_owned(name, with_body) {
221 headers.append(name, value.clone());
222 }
223 }
224 let [authorization, accept, user_agent, sdk, runtime] = PROTECTED_HEADERS;
225 headers.insert(authorization, config.authorization().clone());
226 headers.insert(accept, JSON_CONTENT_TYPE);
227 headers.insert(user_agent, config.user_agent().clone());
228 headers.insert(sdk, SDK_IDENTIFIER);
229 if config.send_runtime_header() {
230 headers.insert(runtime, RUNTIME_IDENTIFIER.clone());
231 }
232 if with_body {
233 headers.insert(CONTENT_TYPE, JSON_CONTENT_TYPE);
234 }
235 headers
236}
237
238/// Whether the SDK or its transport owns `name` on a request, so a caller
239/// cannot set it.
240fn is_sdk_owned(name: &HeaderName, with_body: bool) -> bool {
241 PROTECTED_HEADERS.contains(name)
242 || *name == RETRY_COUNT_HEADER
243 || (with_body && *name == CONTENT_TYPE)
244 || TRANSPORT_HEADERS.contains(name)
245}
246
247/// Parses the headers a caller set on one call, dropping the ones the SDK
248/// owns.
249///
250/// A later header of a name replaces an earlier one, as a later key does in a
251/// Python mapping. The protected headers, `X-TypeSafe-Retry-Count`, and
252/// `Content-Type` on a request with a body are dropped without an error, as
253/// the Python SDK overrides them. The framing and connection headers
254/// ([`TRANSPORT_HEADERS`]) are dropped the same way: they belong to the
255/// transport. `Host` is kept.
256///
257/// # Errors
258///
259/// Returns an [`ErrorKind::InvalidRequest`](crate::ErrorKind::InvalidRequest)
260/// error when a name is not a valid header name or a value is not a valid
261/// header value. The message names the header and never repeats a value.
262pub(crate) fn call_headers<'a, I>(
263 raw: I,
264 with_body: bool,
265) -> Result<Vec<(HeaderName, HeaderValue)>, Error>
266where
267 I: IntoIterator<Item = (&'a str, &'a str)>,
268 I::IntoIter: ExactSizeIterator,
269{
270 let raw = raw.into_iter();
271 let mut parsed: Vec<(HeaderName, HeaderValue)> = Vec::with_capacity(raw.len());
272 for (name, value) in raw {
273 let (name, value) = parse_header(name, value, "").map_err(Error::invalid_request)?;
274 if is_sdk_owned(&name, with_body) {
275 continue;
276 }
277 upsert(&mut parsed, name, value);
278 }
279 Ok(parsed)
280}
281
282/// Parses one header, or says which part of it is not valid.
283///
284/// The message names the header by its name - escaped, and cut at 128
285/// characters, since a name that fails here can be anything - and never
286/// repeats the value: a value is where a caller puts a token. `whose` is the word before `header` in the message: `default ` for
287/// a client default, empty for a per-call header.
288pub(crate) fn parse_header(
289 name: &str,
290 value: &str,
291 whose: &str,
292) -> Result<(HeaderName, HeaderValue), String> {
293 let parsed = HeaderName::from_bytes(name.as_bytes()).map_err(|_| {
294 format!("The {whose}header name {} is not a valid HTTP header name.", text::quoted(name))
295 })?;
296 let value = HeaderValue::from_str(value).map_err(|_| {
297 format!(
298 "The value of the {whose}header {} is not a valid HTTP header value.",
299 text::quoted(name)
300 )
301 })?;
302 Ok((parsed, value))
303}
304
305// ------------------------------------------------------------ one attempt
306
307/// Everything one request needs that does not change between its attempts.
308#[derive(Clone, Copy)]
309pub(crate) struct Exchange<'a> {
310 pub(crate) method: &'a Method,
311 pub(crate) uri: &'a Uri,
312 /// The client's headers for this kind of request; see [`base_headers`].
313 pub(crate) base_headers: &'a HeaderMap,
314 /// The call's own headers, already parsed; see [`call_headers`].
315 pub(crate) call_headers: &'a [(HeaderName, HeaderValue)],
316 /// The deadline of one attempt, or `None` for no deadline.
317 pub(crate) deadline: Option<Duration>,
318 pub(crate) max_response_bytes: usize,
319}
320
321/// What a successful attempt returns: a success status, the headers and the
322/// whole body.
323type Received = (StatusCode, HeaderMap, Bytes);
324
325/// Sends one attempt of a request and reads its response.
326///
327/// `retry` is how many attempts came before this one: from 1 it is sent as
328/// `X-TypeSafe-Retry-Count`, and at 0 the header is absent. `body` is the
329/// finished request body, handed over by value so that a first attempt costs
330/// no copy and a later one only a reference count.
331///
332/// The deadline covers the whole attempt: waiting for the transport to be
333/// ready, connecting, sending, and reading the response body.
334///
335/// # Errors
336///
337/// - [`ErrorKind::Timeout`](crate::ErrorKind::Timeout) when the deadline
338/// passes first.
339/// - [`ErrorKind::Api`](crate::ErrorKind::Api) for any status outside 2xx.
340/// - [`ErrorKind::Connection`](crate::ErrorKind::Connection) when the
341/// transport fails or the body cannot be read; the transport's own error, or
342/// its redacted copy when it held a credential of the request, is the
343/// [`source`](StdError::source).
344/// - [`ErrorKind::ResponseTooLarge`](crate::ErrorKind::ResponseTooLarge) when
345/// a success response's body is larger than the limit. A body over the
346/// limit is not read past it, and a failure response whose body is over the
347/// limit is an API error with the status and headers and no body.
348pub(crate) async fn attempt<S>(
349 service: &S,
350 exchange: Exchange<'_>,
351 retry: u32,
352 body: Option<Bytes>,
353) -> Result<Received, Error>
354where
355 S: HttpService,
356{
357 let events = telemetry::Exchange::new(exchange.method, exchange.uri, retry);
358 // The header map and the request are built inside a block so that their
359 // storage ends before the await. An async function keeps every local of
360 // a scope that is still open when it suspends, even one whose value was
361 // moved out, so written at the top level the two would ride in the
362 // future beside the copy the transport call already holds: 352 bytes of
363 // every call's future, which over a custom transport tips it over the
364 // size at which tokio boxes a spawned future in a debug build (2,048
365 // bytes). Over the default transport hyper's response future keeps the
366 // call over that size either way (2,344 bytes), so a debug-build call
367 // spawned on the default client is boxed.
368 let (started, exchanged) = {
369 let mut headers = exchange.base_headers.clone();
370 for (name, value) in exchange.call_headers {
371 headers.insert(name.clone(), value.clone());
372 }
373 if retry > 0 {
374 headers.insert(RETRY_COUNT_HEADER, HeaderValue::from(retry));
375 }
376 telemetry::sending(events, &headers, body.as_ref());
377 let started = telemetry::clock();
378
379 let mut request = Request::new(body.map_or_else(Body::empty, Body::from));
380 *request.method_mut() = exchange.method.clone();
381 *request.uri_mut() = exchange.uri.clone();
382 *request.headers_mut() = headers;
383 (started, exchange_once(service, request, exchange.max_response_bytes))
384 };
385 let outcome = match exchange.deadline {
386 Some(deadline) => tokio::time::timeout(deadline, exchanged)
387 .await
388 .unwrap_or_else(|_| Err(Failure::Error(Error::timeout(deadline)))),
389 None => exchanged.await,
390 };
391
392 // A response is reported by its status; an attempt that ended without
393 // one is reported by what ended it. An API error is not reported a second
394 // time as a failure: its message can come from the body.
395 let failure = match outcome {
396 Ok((status, headers, body)) => {
397 telemetry::responded(events, status, &headers, started);
398 telemetry::received(events, status, &headers, &body, started);
399 if status.is_success() {
400 return Ok((status, headers, body));
401 }
402 return Err(ApiError::new(status, body, headers, Some(endpoint(exchange))).into());
403 }
404 Err(Failure::TooLarge { status, headers }) if !status.is_success() => {
405 telemetry::responded(events, status, &headers, started);
406 return Err(ApiError::with_message(
407 status,
408 Bytes::new(),
409 headers,
410 Some(endpoint(exchange)),
411 Error::response_too_large(exchange.max_response_bytes).to_string(),
412 )
413 .into());
414 }
415 Err(Failure::TooLarge { .. }) => Error::response_too_large(exchange.max_response_bytes),
416 Err(Failure::Error(error)) => redacted(error, exchange),
417 };
418 telemetry::failed(events, &failure, started);
419 Err(failure)
420}
421
422/// How an attempt can end short of a response body.
423enum Failure {
424 /// Anything that becomes an [`Error`] without needing the response.
425 Error(Error),
426 /// The body was larger than the limit. Whether that is a response too
427 /// large or an API error depends on the status, which is kept.
428 TooLarge { status: StatusCode, headers: HeaderMap },
429}
430
431/// Waits for the transport, sends `request`, and reads the whole response.
432async fn exchange_once<S>(
433 service: &S,
434 request: Request<Body>,
435 limit: usize,
436) -> Result<Received, Failure>
437where
438 S: HttpService,
439{
440 // A clone per call, as `tower` intends: readiness belongs to the handle
441 // that is then called, and a shared one would let another task take the
442 // slot this one waited for. The handle is dropped once the call has been
443 // made: the response future owns what it needs, and a handle kept to the
444 // end would be stored in this future through the whole body read.
445 let called = {
446 let mut service = service.clone();
447 poll_fn(|cx| service.poll_ready(cx))
448 .await
449 .map_err(|error| Failure::Error(connection(error)))?;
450 service.call(request)
451 };
452 let response = called.await.map_err(|error| Failure::Error(connection(error)))?;
453 let (parts, body) = response.into_parts();
454
455 // A declared length over the limit is refused before a byte is read.
456 if http_body::Body::size_hint(&body).lower() > limit as u64 {
457 return Err(Failure::TooLarge { status: parts.status, headers: parts.headers });
458 }
459 match Limited::new(body, limit).collect().await {
460 Ok(collected) => Ok((parts.status, parts.headers, collected.to_bytes())),
461 Err(error) if error.is::<LengthLimitError>() => {
462 Err(Failure::TooLarge { status: parts.status, headers: parts.headers })
463 }
464 Err(error) => Err(Failure::Error(connection(error))),
465 }
466}
467
468/// The error a transport failure becomes.
469///
470/// An error this crate raised inside its own transport is passed through as
471/// it is. Anything else is a connection failure whose message is the chain of
472/// the transport's own messages - the cause stays reachable as the
473/// [`source`](StdError::source).
474pub(crate) fn connection(error: impl Into<BoxError>) -> Error {
475 match error.into().downcast::<Error>() {
476 Ok(ours) => *ours,
477 Err(other) => Error::connection(connection_message(&*other), Some(other)),
478 }
479}
480
481/// `Connection error: ` and then every message of the error chain, joined
482/// with `: `.
483///
484/// The chain is the transport's, not the request's: it names what failed -
485/// `tcp connect error: Connection refused`, `invalid peer certificate:
486/// UnknownIssuer` - and holds neither a header nor a body. It is cut after
487/// eight links, which no real chain reaches.
488///
489/// It can still hold text the server chose: an HTTP/2 GOAWAY's debug data
490/// (up to a frame, 16 KiB by default), the subject of a certificate the
491/// platform refused, and whatever a caller's own transport puts in its
492/// `Display`. So every link is written as text this SDK did not write -
493/// control and format characters escaped - and the whole of it after the
494/// prefix is cut at [`text::MAX_MESSAGE_CHARS`] characters and marked
495/// with U+2026; the full chain stays reachable through
496/// [`source`](StdError::source). A backslash is written as it is: h2 and
497/// rustls already print their own text through `Debug`, where a doubled
498/// backslash would only make an escape harder to read, and nothing is ever
499/// parsed back out of this message.
500fn connection_message(error: &(dyn StdError + 'static)) -> String {
501 cut(&render_uncut(error))
502}
503
504/// What every connection error's message starts with.
505const CONNECTION_PREFIX: &str = "Connection error: ";
506
507/// A connection error's message before it is cut: the text, and the byte
508/// offset at which each of its pieces ends. A piece is one escaped character
509/// or the `": "` between two links, and a cut never splits one.
510struct Uncut {
511 text: String,
512 ends: Vec<usize>,
513}
514
515/// The whole message for `error`: [`CONNECTION_PREFIX`], then up to eight
516/// links of the chain, each escaped, joined with `": "`.
517fn render_uncut(error: &(dyn StdError + 'static)) -> Uncut {
518 let mut message = SafeText::after(String::from(CONNECTION_PREFIX), usize::MAX, Backslash::Keep);
519 let mut ends = Vec::new();
520 let mut character_bytes = [0; 4];
521 let mut link = Some(error);
522 for index in 0..8 {
523 let Some(current) = link else { break };
524 if index > 0 {
525 message.fixed(": ");
526 ends.push(message.byte_len());
527 }
528 for character in current.to_string().chars() {
529 message.untrusted(character.encode_utf8(&mut character_bytes), usize::MAX);
530 ends.push(message.byte_len());
531 }
532 link = current.source();
533 }
534 Uncut { text: message.into_string(), ends }
535}
536
537/// `uncut` cut at [`text::MAX_MESSAGE_CHARS`] characters after the prefix,
538/// the last whole piece that fits followed by U+2026.
539fn cut(uncut: &Uncut) -> String {
540 let mut message = String::from(CONNECTION_PREFIX);
541 let mut chars = 0;
542 let mut start = CONNECTION_PREFIX.len();
543 for &end in &uncut.ends {
544 let piece = &uncut.text[start..end];
545 let len = piece.chars().count();
546 if chars + len > text::MAX_MESSAGE_CHARS {
547 message.push('\u{2026}');
548 break;
549 }
550 message.push_str(piece);
551 chars += len;
552 start = end;
553 }
554 message
555}
556
557impl Uncut {
558 /// This message with every form of a credential after the prefix
559 /// replaced by `***`.
560 ///
561 /// It runs over the escaped text, since escaping can form a credential no
562 /// link holds: a tab written as `\t`, or two links joined by `": "`. A
563 /// match that covers part of a piece takes the whole piece, and the
564 /// replacement is one piece, so the cut can neither split it nor leave
565 /// part of the credential before it.
566 fn redacted(&self, credentials: &Credentials) -> Self {
567 let prefix = CONNECTION_PREFIX.len();
568 let mut found = credentials
569 .matches(&self.text[prefix..])
570 .map(|range| range.start + prefix..range.end + prefix);
571 let mut next = found.next();
572 let mut text = String::from(CONNECTION_PREFIX);
573 let mut ends = Vec::with_capacity(self.ends.len());
574 let mut start = prefix;
575 let mut index = 0;
576 while let Some(&end) = self.ends.get(index) {
577 match &next {
578 Some(first) if first.start < end => {
579 let mut group_end = first.end;
580 next = found.next();
581 while let Some(&piece_end) = self.ends.get(index) {
582 index += 1;
583 while let Some(another) = &next
584 && another.start < piece_end
585 {
586 group_end = group_end.max(another.end);
587 next = found.next();
588 }
589 start = piece_end;
590 if piece_end >= group_end {
591 break;
592 }
593 }
594 text.push_str("***");
595 }
596 _ => {
597 text.push_str(&self.text[start..end]);
598 start = end;
599 index += 1;
600 }
601 }
602 ends.push(text.len());
603 }
604 Self { text, ends }
605 }
606}
607
608/// `error` with the request's credentials kept out of it.
609///
610/// Only a connection error with a cause can hold one: its cause is the
611/// transport's error, and its message is built from it. The credentials are
612/// read from the headers the request was built from, and only here, after
613/// the attempt failed. A chain that holds none is returned as it is; see
614/// [`redact::copy_chain`] for the other two outcomes.
615pub(crate) fn redacted(error: Error, exchange: Exchange<'_>) -> Error {
616 let Some(source) =
617 StdError::source(&error).filter(|_| matches!(error.kind(), ErrorKind::Connection))
618 else {
619 return error;
620 };
621 let credentials = Credentials::new(
622 exchange
623 .base_headers
624 .iter()
625 .chain(exchange.call_headers.iter().map(|(name, value)| (name, value))),
626 );
627 // The fixed prefix is the SDK's own text: only what follows it came from
628 // the transport, as the Python SDK redacts the error before prefixing it.
629 let message = error.to_string();
630 let transport_text = message.strip_prefix(CONNECTION_PREFIX).unwrap_or(&message);
631 match redact::copy_chain(source, transport_text, &credentials) {
632 Outcome::Kept => error,
633 Outcome::MessageOnly => {
634 let (_, source) = error.into_parts();
635 let redacted = credentials.redact(transport_text);
636 Error::connection(format!("{CONNECTION_PREFIX}{redacted}"), source)
637 }
638 Outcome::Replaced(link) => {
639 let message = cut(&render_uncut(&link).redacted(&credentials));
640 Error::connection(message, Some(Box::new(link)))
641 }
642 }
643}
644
645/// The endpoint of `exchange` as an error names it.
646fn endpoint(exchange: Exchange<'_>) -> Box<str> {
647 format_endpoint(exchange.method, exchange.uri).into_boxed_str()
648}
649
650#[cfg(test)]
651#[path = "mod_tests.rs"]
652mod tests;