Skip to content

Commit 853e959

Browse files
authored
app: Set the l5d-proxy-error header on synthesized responses (#1119)
This change adds an `l5d-proxy-error` header to responses that fail with a proxying error, such as fail-fast errors, connection timeouts, etc. When the proxy synthesizes a response for these failed requests, the `l5d-proxy-error` is set with an error message. Currently this is just an informational header. In a future change, we'll update outbound proxies to handle such failures as if they were generated locally. Signed-off-by: Kevin Leimkuhler <[email protected]>
1 parent 63700ed commit 853e959

File tree

5 files changed

+348
-26
lines changed

5 files changed

+348
-26
lines changed

linkerd/app/core/src/errors.rs

Lines changed: 91 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -9,12 +9,15 @@ use linkerd_proxy_http::{ClientHandle, HasH2Reason};
99
use linkerd_timeout::{FailFastError, ResponseTimeout};
1010
use linkerd_tls as tls;
1111
use pin_project::pin_project;
12+
use std::fmt;
1213
use std::pin::Pin;
1314
use std::task::{Context, Poll};
1415
use thiserror::Error;
1516
use tonic::{self as grpc, Code};
1617
use tracing::{debug, warn};
1718

19+
pub const L5D_PROXY_ERROR: &str = "l5d-proxy-error";
20+
1821
metrics! {
1922
inbound_http_errors_total: Counter {
2023
"The total number of inbound HTTP requests that could not be processed due to a proxy error."
@@ -44,7 +47,7 @@ pub struct LabelError(());
4447
#[derive(Copy, Clone, Debug, Error)]
4548
#[error("{}", self.message)]
4649
pub struct HttpError {
47-
http: http::StatusCode,
50+
http: StatusCode,
4851
grpc: Code,
4952
message: &'static str,
5053
reason: Reason,
@@ -217,8 +220,12 @@ impl<RspB: Default + hyper::body::HttpBody> respond::Respond<http::Response<RspB
217220
close.close();
218221
}
219222

223+
// Set the l5d error header on all responses.
224+
let mut builder = http::Response::builder();
225+
builder = set_l5d_proxy_error_header(builder, &*error);
226+
220227
if self.is_grpc {
221-
let mut rsp = http::Response::builder()
228+
let mut rsp = builder
222229
.version(http::Version::HTTP_2)
223230
.header(http::header::CONTENT_LENGTH, "0")
224231
.header(http::header::CONTENT_TYPE, GRPC_CONTENT_TYPE)
@@ -229,32 +236,83 @@ impl<RspB: Default + hyper::body::HttpBody> respond::Respond<http::Response<RspB
229236
return Ok(rsp);
230237
}
231238

232-
let status = http_status(&*error);
233-
debug!(%status, version = ?self.version, "Handling error with HTTP response");
234-
Ok(http::Response::builder()
239+
let rsp = set_http_status(builder, &*error)
235240
.version(self.version)
236-
.status(status)
237241
.header(http::header::CONTENT_LENGTH, "0")
238242
.body(ResponseBody::default())
239-
.expect("error response must be valid"))
243+
.expect("error response must be valid");
244+
let status = rsp.status();
245+
debug!(%status, version = ?self.version, "Handling error with HTTP response");
246+
Ok(rsp)
240247
}
241248
}
242249
}
243250
}
244251

245-
fn http_status(error: &(dyn std::error::Error + 'static)) -> StatusCode {
252+
fn set_l5d_proxy_error_header(
253+
mut builder: http::response::Builder,
254+
error: &(dyn std::error::Error + 'static),
255+
) -> http::response::Builder {
256+
if let Some(HttpError { message, .. }) = error.downcast_ref::<HttpError>() {
257+
builder.header(L5D_PROXY_ERROR, HeaderValue::from_static(message))
258+
} else if error.is::<ResponseTimeout>() {
259+
builder.header(
260+
L5D_PROXY_ERROR,
261+
HeaderValue::from_static("request timed out"),
262+
)
263+
} else if error.is::<ConnectTimeout>() {
264+
builder.header(
265+
L5D_PROXY_ERROR,
266+
HeaderValue::from_static("failed to connect"),
267+
)
268+
} else if let Some(e) = error.downcast_ref::<FailFastError>() {
269+
builder.header(
270+
L5D_PROXY_ERROR,
271+
HeaderValue::from_str(&e.to_string()).unwrap_or_else(|error| {
272+
warn!(%error, "Failed to encode fail-fast error message");
273+
HeaderValue::from_static("service in fail-fast")
274+
}),
275+
)
276+
} else if error.is::<tower::timeout::error::Elapsed>() {
277+
builder.header(
278+
L5D_PROXY_ERROR,
279+
HeaderValue::from_static("proxy dispatch timed out"),
280+
)
281+
} else if error.is::<IdentityRequired>() {
282+
if let Ok(msg) = HeaderValue::from_str(&error.to_string()) {
283+
builder = builder.header(L5D_PROXY_ERROR, msg)
284+
}
285+
builder
286+
} else if let Some(source) = error.source() {
287+
set_l5d_proxy_error_header(builder, source)
288+
} else {
289+
builder.header(
290+
L5D_PROXY_ERROR,
291+
HeaderValue::from_static("proxy received invalid response"),
292+
)
293+
}
294+
}
295+
296+
fn set_http_status(
297+
builder: http::response::Builder,
298+
error: &(dyn std::error::Error + 'static),
299+
) -> http::response::Builder {
246300
if let Some(HttpError { http, .. }) = error.downcast_ref::<HttpError>() {
247-
*http
301+
builder.status(*http)
248302
} else if error.is::<ResponseTimeout>() {
249-
http::StatusCode::GATEWAY_TIMEOUT
250-
} else if error.is::<FailFastError>() || error.is::<tower::timeout::error::Elapsed>() {
251-
http::StatusCode::SERVICE_UNAVAILABLE
303+
builder.status(StatusCode::GATEWAY_TIMEOUT)
304+
} else if error.is::<ConnectTimeout>() {
305+
builder.status(StatusCode::GATEWAY_TIMEOUT)
306+
} else if error.is::<FailFastError>() {
307+
builder.status(StatusCode::SERVICE_UNAVAILABLE)
308+
} else if error.is::<tower::timeout::error::Elapsed>() {
309+
builder.status(StatusCode::SERVICE_UNAVAILABLE)
252310
} else if error.is::<IdentityRequired>() {
253-
http::StatusCode::FORBIDDEN
311+
builder.status(StatusCode::FORBIDDEN)
254312
} else if let Some(source) = error.source() {
255-
http_status(source)
313+
set_http_status(builder, source)
256314
} else {
257-
http::StatusCode::BAD_GATEWAY
315+
builder.status(StatusCode::BAD_GATEWAY)
258316
}
259317
}
260318

@@ -346,8 +404,8 @@ pub struct IdentityRequired {
346404
pub found: Option<tls::client::ServerId>,
347405
}
348406

349-
impl std::fmt::Display for IdentityRequired {
350-
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
407+
impl fmt::Display for IdentityRequired {
408+
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
351409
match self.found {
352410
Some(ref found) => write!(
353411
f,
@@ -396,7 +454,7 @@ impl error_metrics::LabelError<Error> for LabelError {
396454
}
397455

398456
impl FmtLabels for Reason {
399-
fn fmt_labels(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
457+
fn fmt_labels(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
400458
write!(
401459
f,
402460
"message=\"{}\"",
@@ -447,7 +505,7 @@ impl HttpError {
447505
pub fn identity_required(message: &'static str) -> Self {
448506
Self {
449507
message,
450-
http: http::StatusCode::FORBIDDEN,
508+
http: StatusCode::FORBIDDEN,
451509
grpc: Code::Unauthenticated,
452510
reason: Reason::IdentityRequired,
453511
}
@@ -456,7 +514,7 @@ impl HttpError {
456514
pub fn not_found(message: &'static str) -> Self {
457515
Self {
458516
message,
459-
http: http::StatusCode::NOT_FOUND,
517+
http: StatusCode::NOT_FOUND,
460518
grpc: Code::NotFound,
461519
reason: Reason::NotFound,
462520
}
@@ -465,13 +523,24 @@ impl HttpError {
465523
pub fn gateway_loop() -> Self {
466524
Self {
467525
message: "gateway loop detected",
468-
http: http::StatusCode::LOOP_DETECTED,
526+
http: StatusCode::LOOP_DETECTED,
469527
grpc: Code::Aborted,
470528
reason: Reason::GatewayLoop,
471529
}
472530
}
473531

474-
pub fn status(&self) -> http::StatusCode {
532+
pub fn status(&self) -> StatusCode {
475533
self.http
476534
}
477535
}
536+
537+
#[derive(Debug)]
538+
pub(crate) struct ConnectTimeout(pub std::time::Duration);
539+
540+
impl fmt::Display for ConnectTimeout {
541+
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
542+
write!(f, "connect timed out after {:?}", self.0)
543+
}
544+
}
545+
546+
impl std::error::Error for ConnectTimeout {}

linkerd/app/core/src/svc.rs

Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -174,6 +174,28 @@ impl<S> Stack<S> {
174174
self.push(tower::timeout::TimeoutLayer::new(timeout))
175175
}
176176

177+
/// Wraps the inner service with a response timeout such that timeout errors are surfaced as a
178+
/// `ConnectTimeout` error.
179+
///
180+
/// Note that any timeouts errors from the inner service will be wrapped as well.
181+
pub fn push_connect_timeout<T>(
182+
self,
183+
timeout: Duration,
184+
) -> Stack<stack::MapErr<tower::timeout::Timeout<S>, impl FnOnce(Error) -> Error + Clone>>
185+
where
186+
S: Service<T>,
187+
S::Error: Into<Error>,
188+
{
189+
self.push_timeout(timeout)
190+
.push(MapErrLayer::new(move |err: Error| {
191+
if err.is::<tower::timeout::error::Elapsed>() {
192+
crate::errors::ConnectTimeout(timeout).into()
193+
} else {
194+
err
195+
}
196+
}))
197+
}
198+
177199
pub fn push_http_insert_target<P>(self) -> Stack<http::insert::NewInsert<P, S>> {
178200
self.push(http::insert::NewInsert::layer())
179201
}

linkerd/app/inbound/src/http/mod.rs

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -94,7 +94,7 @@ where
9494
C: svc::Service<TcpEndpoint> + Clone + Send + Sync + Unpin + 'static,
9595
C::Response: io::AsyncRead + io::AsyncWrite + Send + Unpin + 'static,
9696
C::Error: Into<Error>,
97-
C::Future: Send + Unpin,
97+
C::Future: Send,
9898
{
9999
pub fn push_http_router<P>(
100100
self,
@@ -118,6 +118,7 @@ where
118118
self.map_stack(|config, rt, connect| {
119119
// Creates HTTP clients for each inbound port & HTTP settings.
120120
let endpoint = connect
121+
.push(svc::stack::BoxFuture::layer())
121122
.push(rt.metrics.transport.layer_connect())
122123
.push_map_target(TcpEndpoint::from)
123124
.push(http::client::layer(

0 commit comments

Comments
 (0)