use bytes::Bytes;
use http_body_util::Full;
use hyper::{Response, StatusCode};
use tracing::warn;
#[derive(Debug, thiserror::Error)]
pub enum ProxyError {
#[error("configuration error: {0}")]
Config(String),
#[error("invalid upstream: {0}")]
InvalidUpstream(String),
#[error("blocked header: {0}")]
BlockedHeader(String),
#[error("blocked parameter: {0}")]
BlockedParam(String),
#[error("request body exceeds {limit} byte limit")]
BodyTooLarge {
limit: u64,
},
#[error("ambiguous request framing: both Content-Length and Transfer-Encoding present")]
RequestSmuggling,
#[error("upstream error: {0}")]
Upstream(#[from] hyper_util::client::legacy::Error),
#[error("http error: {0}")]
Http(#[from] hyper::http::Error),
#[error("invalid header value: {0}")]
InvalidHeaderValue(#[from] hyper::header::InvalidHeaderValue),
#[error("invalid header name: {0}")]
InvalidHeaderName(#[from] hyper::header::InvalidHeaderName),
#[error("upstream request timed out after {0:?}")]
Timeout(std::time::Duration),
#[error("service at capacity: {limit} concurrent requests")]
ServiceUnavailable {
limit: usize,
},
#[error("tls error: {0}")]
Tls(String),
#[error("rate limit exceeded, retry after {retry_after_ms}ms")]
RateLimited {
retry_after_ms: u64,
},
#[error("no healthy upstream backend available")]
NoHealthyUpstream,
#[error("internal error: {0}")]
Internal(String),
}
impl ProxyError {
pub fn status_code(&self) -> StatusCode {
match self {
Self::Config(_)
| Self::Internal(_)
| Self::InvalidUpstream(_)
| Self::Http(_)
| Self::InvalidHeaderValue(_)
| Self::InvalidHeaderName(_)
| Self::Tls(_) => StatusCode::INTERNAL_SERVER_ERROR,
Self::NoHealthyUpstream => StatusCode::SERVICE_UNAVAILABLE,
Self::RateLimited { .. } => StatusCode::TOO_MANY_REQUESTS,
Self::BlockedHeader(_) | Self::BlockedParam(_) => StatusCode::FORBIDDEN,
Self::BodyTooLarge { .. } => StatusCode::PAYLOAD_TOO_LARGE,
Self::RequestSmuggling => StatusCode::BAD_REQUEST,
Self::Upstream(_) => StatusCode::BAD_GATEWAY,
Self::Timeout(_) => StatusCode::GATEWAY_TIMEOUT,
Self::ServiceUnavailable { .. } => StatusCode::SERVICE_UNAVAILABLE,
}
}
pub fn into_response(self) -> Response<Full<Bytes>> {
let status = self.status_code();
warn!(
status = status.as_u16(),
error = self.error_tag(),
%self,
"returning error response"
);
let retry_after_ms = match &self {
Self::RateLimited { retry_after_ms } => Some(*retry_after_ms),
_ => None,
};
let body = serde_json::json!({
"error": self.error_tag(),
"message": self.to_string(),
});
let mut builder = Response::builder()
.status(status)
.header("content-type", "application/json");
if let Some(ms) = retry_after_ms {
let retry_secs = ms.div_ceil(1000);
builder = builder.header("retry-after", retry_secs.to_string());
}
builder
.body(Full::new(Bytes::from(body.to_string())))
.unwrap_or_else(|_| {
Response::builder()
.status(StatusCode::INTERNAL_SERVER_ERROR)
.body(Full::new(Bytes::new()))
.expect("building fallback response must not fail")
})
}
fn error_tag(&self) -> &'static str {
match self {
Self::Config(_) => "config_error",
Self::InvalidUpstream(_) => "invalid_upstream",
Self::BlockedHeader(_) => "blocked_header",
Self::BlockedParam(_) => "blocked_param",
Self::BodyTooLarge { .. } => "body_too_large",
Self::RequestSmuggling => "request_smuggling",
Self::Upstream(_) => "upstream_error",
Self::Timeout(_) => "gateway_timeout",
Self::ServiceUnavailable { .. } => "service_unavailable",
Self::Http(_) | Self::InvalidHeaderValue(_) | Self::InvalidHeaderName(_) => {
"http_error"
}
Self::Tls(_) => "tls_error",
Self::RateLimited { .. } => "rate_limited",
Self::NoHealthyUpstream => "no_healthy_upstream",
Self::Internal(_) => "internal_error",
}
}
}