diff --git a/actix-http/CHANGES.md b/actix-http/CHANGES.md index a58846405..d03a45969 100644 --- a/actix-http/CHANGES.md +++ b/actix-http/CHANGES.md @@ -1,10 +1,14 @@ # Changes ## Unreleased - 2021-xx-xx +### Added +- Response headers can be sent as camel case using `res.head_mut().set_camel_case_headers(true)`. [#2587] + ### Changed - Brotli (de)compression support is now provided by the `brotli` crate. [#2538] [#2538]: https://github.com/actix/actix-web/pull/2538 +[#2587]: https://github.com/actix/actix-web/pull/2587 ## 3.0.0-beta.18 - 2022-01-04 diff --git a/actix-http/Cargo.toml b/actix-http/Cargo.toml index df9e11419..163fce931 100644 --- a/actix-http/Cargo.toml +++ b/actix-http/Cargo.toml @@ -88,6 +88,7 @@ async-stream = "0.3" criterion = { version = "0.3", features = ["html_reports"] } env_logger = "0.9" futures-util = { version = "0.3.7", default-features = false, features = ["alloc"] } +memchr = "2.4" rcgen = "0.8" regex = "1.3" rustls-pemfile = "0.2" diff --git a/actix-http/src/h1/encoder.rs b/actix-http/src/h1/encoder.rs index f2a862278..8b1e3b623 100644 --- a/actix-http/src/h1/encoder.rs +++ b/actix-http/src/h1/encoder.rs @@ -258,6 +258,12 @@ impl MessageType for Response<()> { None } + fn camel_case(&self) -> bool { + self.head() + .flags + .contains(crate::message::Flags::CAMEL_CASE) + } + fn encode_status(&mut self, dst: &mut BytesMut) -> io::Result<()> { let head = self.head(); let reason = head.reason().as_bytes(); diff --git a/actix-http/src/responses/head.rs b/actix-http/src/responses/head.rs index 78d9536e5..d11ba8fde 100644 --- a/actix-http/src/responses/head.rs +++ b/actix-http/src/responses/head.rs @@ -20,7 +20,7 @@ pub struct ResponseHead { pub headers: HeaderMap, pub reason: Option<&'static str>, pub(crate) extensions: RefCell, - flags: Flags, + pub(crate) flags: Flags, } impl ResponseHead { @@ -49,6 +49,18 @@ impl ResponseHead { &mut self.headers } + /// Sets the flag that controls wether to send headers formatted as Camel-Case. + /// + /// Only applicable to HTTP/1.x responses; HTTP/2 header names are always lowercase. + #[inline] + pub fn set_camel_case_headers(&mut self, camel_case: bool) { + if camel_case { + self.flags.insert(Flags::CAMEL_CASE); + } else { + self.flags.remove(Flags::CAMEL_CASE); + } + } + /// Message extensions #[inline] pub fn extensions(&self) -> Ref<'_, Extensions> { @@ -206,3 +218,57 @@ impl BoxedResponsePool { } } } + +#[cfg(test)] +mod tests { + use std::{ + io::{Read as _, Write as _}, + net, + }; + + use memchr::memmem; + + use crate::{ + header::{HeaderName, HeaderValue}, + Error, HttpService, Request, Response, + }; + + #[actix_rt::test] + async fn camel_case_headers() { + let mut srv = actix_http_test::test_server(|| { + HttpService::new(|req: Request| async move { + let mut res = Response::ok(); + + if req.path().contains("camel") { + res.head_mut().set_camel_case_headers(true); + } + + res.headers_mut().insert( + HeaderName::from_static("foo-bar"), + HeaderValue::from_static("baz"), + ); + Ok::<_, Error>(res) + }) + .tcp() + }) + .await; + + let mut stream = net::TcpStream::connect(srv.addr()).unwrap(); + let _ = stream.write_all(b"GET /camel HTTP/1.1\r\nConnection: Close\r\n\r\n"); + let mut data = vec![0; 1024]; + let _ = stream.read(&mut data); + assert_eq!(&data[..17], b"HTTP/1.1 200 OK\r\n"); + assert!(memmem::find(&data, b"Foo-Bar").is_some()); + assert!(!memmem::find(&data, b"foo-bar").is_some()); + + let mut stream = net::TcpStream::connect(srv.addr()).unwrap(); + let _ = stream.write_all(b"GET /lower HTTP/1.1\r\nConnection: Close\r\n\r\n"); + let mut data = vec![0; 1024]; + let _ = stream.read(&mut data); + assert_eq!(&data[..17], b"HTTP/1.1 200 OK\r\n"); + assert!(!memmem::find(&data, b"Foo-Bar").is_some()); + assert!(memmem::find(&data, b"foo-bar").is_some()); + + srv.stop().await; + } +}