use axum::body::Body;
use axum::http::header::{Entry, OccupiedEntry, CONTENT_TYPE};
use axum::http::{HeaderMap, HeaderName, HeaderValue, StatusCode};
use axum::response::Response;
use tonic::metadata::MetadataMap;
pub(crate) const HTTP_CODE_KEY: &str = "x-http-code";
fn is_withheld(name: &str) -> bool {
name.starts_with("grpc-")
|| name.ends_with("-bin")
|| matches!(
name,
"content-type"
| "connection"
| "keep-alive"
| "proxy-connection"
| "te"
| "trailer"
| "transfer-encoding"
| "upgrade"
| "content-length"
)
}
#[derive(Debug, Default, Clone, Copy, PartialEq, Eq)]
enum HttpCode {
#[default]
Absent,
Set(StatusCode),
Invalid,
}
#[derive(Debug, PartialEq, Eq)]
pub(crate) struct InvalidHttpCode;
#[derive(Debug, Default)]
pub(crate) struct UpstreamHeaders {
headers: HeaderMap,
http_code: HttpCode,
}
enum Current<'a> {
Forward(OccupiedEntry<'a, HeaderValue>),
HttpCode,
Drop,
}
impl UpstreamHeaders {
pub(crate) fn absorb(&mut self, metadata: MetadataMap, deny: &[HeaderName]) {
let headers = &mut self.headers;
let http_code = &mut self.http_code;
let mut current = Current::Drop;
for (name, value) in metadata.into_headers() {
if let Some(name) = name {
current = Current::Drop;
if name.as_str() == HTTP_CODE_KEY {
record_http_code(http_code, &value);
current = Current::HttpCode;
} else if !is_withheld(name.as_str()) && !deny.contains(&name) {
current = Current::Forward(match headers.entry(name) {
Entry::Occupied(mut entry) => {
entry.append(value);
entry
}
Entry::Vacant(entry) => entry.insert_entry(value),
});
}
continue;
}
match &mut current {
Current::Forward(entry) => entry.append(value),
Current::HttpCode => record_http_code(http_code, &value),
Current::Drop => {}
}
}
}
pub(crate) fn status(&self) -> Result<Option<StatusCode>, InvalidHttpCode> {
match self.http_code {
HttpCode::Absent => Ok(None),
HttpCode::Set(status) => Ok(Some(status)),
HttpCode::Invalid => Err(InvalidHttpCode),
}
}
pub(crate) fn into_headers(self) -> HeaderMap {
self.headers
}
}
fn record_http_code(slot: &mut HttpCode, value: &HeaderValue) {
*slot = match (*slot, parse_http_code(value)) {
(HttpCode::Absent, Some(status)) => HttpCode::Set(status),
_ => HttpCode::Invalid,
};
}
fn parse_http_code(value: &HeaderValue) -> Option<StatusCode> {
let bytes = value.as_bytes();
if bytes.len() != 3 || !bytes.iter().all(u8::is_ascii_digit) {
return None;
}
let code = bytes
.iter()
.fold(0u16, |code, digit| code * 10 + u16::from(digit - b'0'));
if !(200..=599).contains(&code) {
return None;
}
StatusCode::from_u16(code).ok()
}
pub(crate) fn with_upstream_headers(response: Response, upstream: HeaderMap) -> Response {
if upstream.is_empty() {
return response;
}
let (mut parts, body) = response.into_parts();
let own = std::mem::replace(&mut parts.headers, upstream);
let mut last: Option<HeaderName> = None;
for (name, value) in own {
match name {
Some(name) => {
parts.headers.insert(&name, value);
last = Some(name);
}
None => {
if let Some(name) = &last {
parts.headers.append(name, value);
}
}
}
}
Response::from_parts(parts, body)
}
pub(crate) fn build(
status: StatusCode,
mut headers: HeaderMap,
content_type: Option<HeaderValue>,
body: Body,
) -> Response {
let no_content = matches!(
status,
StatusCode::NO_CONTENT | StatusCode::RESET_CONTENT | StatusCode::NOT_MODIFIED
);
let body = if no_content { Body::empty() } else { body };
if let (Some(content_type), false) = (content_type, no_content) {
headers.insert(CONTENT_TYPE, content_type);
}
let mut response = Response::new(body);
*response.status_mut() = status;
*response.headers_mut() = headers;
response
}
#[cfg(test)]
mod tests;