diff options
author | Yuchen Wu <[email protected]> | 2024-05-01 14:49:05 -0700 |
---|---|---|
committer | Yuchen Wu <[email protected]> | 2024-05-10 15:22:37 -0700 |
commit | 1b7d43207b11fbd893c313c5d312dbafe5d53241 (patch) | |
tree | 88ab17e1629fc6143a2befd85a5b7a85f3c29b54 | |
parent | 4347ef447183a66508c4de649a0657d1ce367dab (diff) | |
download | pingora-1b7d43207b11fbd893c313c5d312dbafe5d53241.tar.gz pingora-1b7d43207b11fbd893c313c5d312dbafe5d53241.zip |
parse and send HTTP/1 reason phrase
This change allows customized HTTP1 reason phrase to be used and
proxied.
-rw-r--r-- | .bleep | 2 | ||||
-rw-r--r-- | pingora-core/src/protocols/http/v1/client.rs | 16 | ||||
-rw-r--r-- | pingora-core/src/protocols/http/v1/server.rs | 17 | ||||
-rw-r--r-- | pingora-http/src/lib.rs | 45 |
4 files changed, 78 insertions, 2 deletions
@@ -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"); + } } |