aboutsummaryrefslogtreecommitdiffhomepage
diff options
context:
space:
mode:
authorYuchen Wu <[email protected]>2024-05-01 14:49:05 -0700
committerYuchen Wu <[email protected]>2024-05-01 16:36:16 -0700
commite7f9c9276f167edbc68cddc2231422bb66d2c3b1 (patch)
tree88ab17e1629fc6143a2befd85a5b7a85f3c29b54
parentf408d2247ba7789024ce3eed1006c2dd33920122 (diff)
downloadpingora-e7f9c9276f167edbc68cddc2231422bb66d2c3b1.tar.gz
pingora-e7f9c9276f167edbc68cddc2231422bb66d2c3b1.zip
parse and send HTTP/1 reason phrase
This change allows customized HTTP1 reason phrase to be used and proxied.
-rw-r--r--.bleep2
-rw-r--r--pingora-core/src/protocols/http/v1/client.rs16
-rw-r--r--pingora-core/src/protocols/http/v1/server.rs17
-rw-r--r--pingora-http/src/lib.rs45
4 files changed, 78 insertions, 2 deletions
diff --git a/.bleep b/.bleep
index f883866..2334b4d 100644
--- a/.bleep
+++ b/.bleep
@@ -1 +1 @@
-b5cb1b6f679bf1dcd136617b0212eb4f7c4bce84 \ No newline at end of file
+c6dc070f9cc16937943cc3b4d3299dbe3904fab8 \ No newline at end of file
diff --git a/pingora-core/src/protocols/http/v1/client.rs b/pingora-core/src/protocols/http/v1/client.rs
index 1dfb97a..aaa7d8c 100644
--- a/pingora-core/src/protocols/http/v1/client.rs
+++ b/pingora-core/src/protocols/http/v1/client.rs
@@ -249,6 +249,8 @@ impl HttpSession {
_ => Version::HTTP_09,
});
+ response_header.set_reason_phrase(resp.reason)?;
+
let buf = buf.freeze();
for header in header_refs {
@@ -721,6 +723,20 @@ mod tests_stream {
}
#[tokio::test]
+ async fn read_response_custom_reason() {
+ init_log();
+ let input = b"HTTP/1.1 200 Just Fine\r\n\r\n";
+ let mock_io = Builder::new().read(&input[..]).build();
+ let mut http_stream = HttpSession::new(Box::new(mock_io));
+ let res = http_stream.read_response().await;
+ assert_eq!(input.len(), res.unwrap());
+ assert_eq!(
+ http_stream.resp_header().unwrap().get_reason_phrase(),
+ Some("Just Fine")
+ );
+ }
+
+ #[tokio::test]
async fn read_response_default() {
init_log();
let input_header = b"HTTP/1.1 200 OK\r\n\r\n";
diff --git a/pingora-core/src/protocols/http/v1/server.rs b/pingora-core/src/protocols/http/v1/server.rs
index 7939ed4..d808506 100644
--- a/pingora-core/src/protocols/http/v1/server.rs
+++ b/pingora-core/src/protocols/http/v1/server.rs
@@ -1013,7 +1013,7 @@ fn http_resp_header_to_buf(
let status = resp.status;
buf.put_slice(status.as_str().as_bytes());
buf.put_u8(b' ');
- let reason = status.canonical_reason();
+ let reason = resp.get_reason_phrase();
if let Some(reason_buf) = reason {
buf.put_slice(reason_buf.as_bytes());
}
@@ -1382,6 +1382,21 @@ mod tests_stream {
}
#[tokio::test]
+ async fn write_custom_reason() {
+ let wire = b"HTTP/1.1 200 Just Fine\r\nFoo: Bar\r\n\r\n";
+ let mock_io = Builder::new().write(wire).build();
+ let mut http_stream = HttpSession::new(Box::new(mock_io));
+ let mut new_response = ResponseHeader::build(StatusCode::OK, None).unwrap();
+ new_response.set_reason_phrase(Some("Just Fine")).unwrap();
+ new_response.append_header("Foo", "Bar").unwrap();
+ http_stream.update_resp_headers = false;
+ http_stream
+ .write_response_header_ref(&new_response)
+ .await
+ .unwrap();
+ }
+
+ #[tokio::test]
async fn write_informational() {
let wire = b"HTTP/1.1 100 Continue\r\n\r\nHTTP/1.1 200 OK\r\nFoo: Bar\r\n\r\n";
let mock_io = Builder::new().write(wire).build();
diff --git a/pingora-http/src/lib.rs b/pingora-http/src/lib.rs
index b616743..74dee53 100644
--- a/pingora-http/src/lib.rs
+++ b/pingora-http/src/lib.rs
@@ -288,6 +288,8 @@ pub struct ResponseHeader {
base: RespParts,
// an ordered header map to store the original case of each header name
header_name_map: Option<CaseMap>,
+ // the reason phrase of the response, if unset, a default one will be used
+ reason_phrase: Option<String>,
}
impl AsRef<RespParts> for ResponseHeader {
@@ -309,6 +311,7 @@ impl Clone for ResponseHeader {
Self {
base: self.as_owned_parts(),
header_name_map: self.header_name_map.clone(),
+ reason_phrase: self.reason_phrase.clone(),
}
}
}
@@ -319,6 +322,7 @@ impl From<RespParts> for ResponseHeader {
Self {
base: parts,
header_name_map: None,
+ reason_phrase: None,
}
}
}
@@ -350,6 +354,7 @@ impl ResponseHeader {
ResponseHeader {
base,
header_name_map: None,
+ reason_phrase: None,
}
}
@@ -443,6 +448,27 @@ impl ResponseHeader {
self.base.version = version
}
+ /// Set the HTTP reason phase. If `None`, a default reason phase will be used
+ pub fn set_reason_phrase(&mut self, reason_phrase: Option<&str>) -> Result<()> {
+ // No need to allocate memory to store the phrase if it is the default one.
+ if reason_phrase == self.base.status.canonical_reason() {
+ self.reason_phrase = None;
+ return Ok(());
+ }
+
+ // TODO: validate it "*( HTAB / SP / VCHAR / obs-text )"
+ self.reason_phrase = reason_phrase.map(str::to_string);
+ Ok(())
+ }
+
+ /// Get the HTTP reason phase. If [Self::set_reason_phrase()] is never called
+ /// or set to `None`, a default reason phase will be used
+ pub fn get_reason_phrase(&self) -> Option<&str> {
+ self.reason_phrase
+ .as_deref()
+ .or_else(|| self.base.status.canonical_reason())
+ }
+
/// Clone `self` into [http::response::Parts].
pub fn as_owned_parts(&self) -> RespParts {
clone_resp_parts(&self.base)
@@ -668,4 +694,23 @@ mod tests {
assert_eq!("Hello�World", req.uri.path_and_query().unwrap());
assert_eq!(raw_path, req.raw_path());
}
+
+ #[test]
+ fn test_reason_phrase() {
+ let mut resp = ResponseHeader::new(None);
+ let reason = resp.get_reason_phrase().unwrap();
+ assert_eq!(reason, "OK");
+
+ resp.set_reason_phrase(Some("FooBar")).unwrap();
+ let reason = resp.get_reason_phrase().unwrap();
+ assert_eq!(reason, "FooBar");
+
+ resp.set_reason_phrase(Some("OK")).unwrap();
+ let reason = resp.get_reason_phrase().unwrap();
+ assert_eq!(reason, "OK");
+
+ resp.set_reason_phrase(None).unwrap();
+ let reason = resp.get_reason_phrase().unwrap();
+ assert_eq!(reason, "OK");
+ }
}