use hyper::header::{HeaderName, HeaderValue};
use hyper::{Response, StatusCode};
use plecto_control::{Header, HttpResponse};
use crate::ResponseBody;
use crate::body::full;
use crate::headers::{copy_headers_direct, copy_headers_synth};
const X_PLECTO_FAULT: HeaderName = HeaderName::from_static("x-plecto-fault");
const RETRY_AFTER: HeaderName = HeaderName::from_static("retry-after");
const X_PLECTO_ERROR_CODE: HeaderName = HeaderName::from_static("x-plecto-error-code");
pub(crate) mod fault {
use hyper::header::HeaderValue;
pub(crate) static BAD_PATH: HeaderValue = HeaderValue::from_static("bad-path");
pub(crate) static NO_ROUTE: HeaderValue = HeaderValue::from_static("no-route");
pub(crate) static RATE_LIMITED: HeaderValue = HeaderValue::from_static("rate-limited");
pub(crate) static NO_HEALTHY_UPSTREAM: HeaderValue =
HeaderValue::from_static("no-healthy-upstream");
pub(crate) static BODY_TOO_LARGE: HeaderValue = HeaderValue::from_static("body-too-large");
pub(crate) static BODY_TIMEOUT: HeaderValue = HeaderValue::from_static("body-timeout");
pub(crate) static BODY_READ_ERROR: HeaderValue = HeaderValue::from_static("body-read-error");
pub(crate) static BODY_BUFFER_UNAVAILABLE: HeaderValue =
HeaderValue::from_static("body-buffer-unavailable");
pub(crate) static CIRCUIT_OPEN: HeaderValue = HeaderValue::from_static("circuit-open");
pub(crate) static REQUEST_TIMEOUT: HeaderValue = HeaderValue::from_static("request-timeout");
pub(crate) static UPSTREAM_TIMEOUT: HeaderValue = HeaderValue::from_static("upstream-timeout");
pub(crate) static UPSTREAM: HeaderValue = HeaderValue::from_static("upstream");
pub(crate) static BAD_UPGRADE: HeaderValue = HeaderValue::from_static("bad-upgrade");
pub(crate) static BAD_CONTENT_LENGTH: HeaderValue =
HeaderValue::from_static("bad-content-length");
}
fn build_error_response() -> Response<ResponseBody> {
let mut resp = Response::new(full(b"response build error".to_vec()));
*resp.status_mut() = StatusCode::BAD_GATEWAY;
resp
}
pub(crate) fn http_response(resp: HttpResponse) -> Response<ResponseBody> {
let status = StatusCode::from_u16(resp.status).unwrap_or(StatusCode::BAD_GATEWAY);
let mut builder = Response::builder().status(status);
copy_headers_synth(builder.headers_mut(), &resp.headers);
builder
.body(full(resp.body))
.unwrap_or_else(|_| build_error_response())
}
pub(crate) fn discard_upstream_body(mut body: ResponseBody) {
const DRAIN_CAP: usize = 64 << 10; tokio::spawn(async move {
use http_body_util::BodyExt;
let mut drained = 0usize;
while drained < DRAIN_CAP {
match body.frame().await {
Some(Ok(frame)) => {
if let Some(data) = frame.data_ref() {
drained = drained.saturating_add(data.len());
}
}
Some(Err(_)) | None => break,
}
}
});
}
pub(crate) fn stream_response(
status: u16,
headers: &[Header],
upstream_headers: &hyper::HeaderMap,
body: ResponseBody,
) -> Response<ResponseBody> {
let status = StatusCode::from_u16(status).unwrap_or(StatusCode::BAD_GATEWAY);
let mut builder = Response::builder().status(status);
copy_headers_synth(builder.headers_mut(), headers);
if let (Some(h), Some(len)) = (
builder.headers_mut(),
upstream_headers.get(hyper::header::CONTENT_LENGTH),
) {
h.insert(hyper::header::CONTENT_LENGTH, len.clone());
}
builder
.body(body)
.unwrap_or_else(|_| build_error_response())
}
pub(crate) fn stream_response_direct(
status: StatusCode,
headers: &hyper::HeaderMap,
body: ResponseBody,
) -> Response<ResponseBody> {
let mut builder = Response::builder().status(status);
copy_headers_direct(builder.headers_mut(), headers);
builder
.body(body)
.unwrap_or_else(|_| build_error_response())
}
pub(crate) fn synth(
status: StatusCode,
fault: &'static HeaderValue,
body: &'static [u8],
) -> Response<ResponseBody> {
let mut resp = Response::new(full(body.to_vec()));
*resp.status_mut() = status;
resp.headers_mut().insert(X_PLECTO_FAULT, fault.clone());
resp
}
pub(crate) fn synth_retry_after(
status: StatusCode,
fault: &'static HeaderValue,
body: &'static [u8],
retry_after_secs: u64,
) -> Response<ResponseBody> {
let mut resp = synth(status, fault, body);
let retry_after = HeaderValue::from_str(&retry_after_secs.to_string())
.unwrap_or_else(|_| HeaderValue::from_static("0"));
resp.headers_mut().insert(RETRY_AFTER, retry_after);
resp
}
pub(crate) fn with_error_code(
mut resp: Response<ResponseBody>,
diagnostic: &plecto_control::Diagnostic,
) -> Response<ResponseBody> {
let code = HeaderValue::from_str(diagnostic.code)
.unwrap_or_else(|_| HeaderValue::from_static("PLECTO-E0000"));
resp.headers_mut().insert(X_PLECTO_ERROR_CODE, code);
resp
}
#[cfg(test)]
mod tests {
use super::*;
fn header(name: &str, value: &str) -> Header {
Header {
name: name.to_string(),
value: value.as_bytes().to_vec(),
}
}
#[test]
fn http_response_clamps_invalid_status_and_drops_invalid_headers_without_panicking() {
for bad_status in [0u16, 99, 1000] {
let resp = http_response(HttpResponse {
status: bad_status,
headers: vec![],
body: Vec::new(),
});
assert_eq!(
resp.status(),
StatusCode::BAD_GATEWAY,
"an out-of-range status ({bad_status}) clamps to 502"
);
}
let resp = http_response(HttpResponse {
status: 403,
headers: vec![header("x-clean", "ok"), header("x-evil", "a\r\nb")],
body: b"denied".to_vec(),
});
assert_eq!(resp.status(), StatusCode::FORBIDDEN);
assert!(resp.headers().contains_key("x-clean"));
assert!(
!resp.headers().contains_key("x-evil"),
"an invalid header value is dropped from a synthesised response"
);
}
#[test]
fn http_response_strips_guest_content_length_so_host_framing_wins() {
let resp = http_response(HttpResponse {
status: 200,
headers: vec![header("content-length", "9999"), header("x-ok", "1")],
body: b"hello".to_vec(),
});
assert!(
!resp.headers().contains_key("content-length"),
"guest Content-Length must not survive onto a synthesised response"
);
assert!(resp.headers().contains_key("x-ok"));
}
#[test]
fn with_error_code_sets_the_plecto_error_code_header() {
let resp = with_error_code(
synth(
StatusCode::TOO_MANY_REQUESTS,
&fault::RATE_LIMITED,
b"limited",
),
&plecto_control::QUOTA_EXCEEDED,
);
assert_eq!(
resp.headers().get("x-plecto-error-code").unwrap(),
"PLECTO-E0002"
);
assert_eq!(
resp.headers().get("x-plecto-fault").unwrap(),
"rate-limited"
);
}
}