Skip to content

Commit 7e2684d

Browse files
zulinx86roypat
authored andcommitted
fix: Always set Content-Length field in non-100/204 responses
The client may wait for the server to close the connection or for timeout to occur in some cases. This commit changes it to always set Content-Length field in non-100/204 responses regardless of whether the body is empty. Although it doesn't cover all the patterns where the response must not set it, users can remove it by calling `set_content_length(None)`. Signed-off-by: Takahiro Itazuri <[email protected]>
1 parent e75dfa1 commit 7e2684d

File tree

2 files changed

+83
-10
lines changed

2 files changed

+83
-10
lines changed

CHANGELOG.md

+3
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,9 @@
1111

1212
- Mark `HttpServer::new_from_fd` as `unsafe` as the correctness of the unsafe code
1313
in this method relies on an invariant the caller has to uphold
14+
- Always set 'Content-Length' in non-100/204 responses regardless of whether the
15+
body is empty. Otherwise, the client waits for the server to close the
16+
connection or for timeout to occur.
1417

1518
# v0.1.0
1619

src/response.rs

+80-10
Original file line numberDiff line numberDiff line change
@@ -86,7 +86,7 @@ impl StatusLine {
8686
/// The content type can be updated with a call to `set_content_type`.
8787
#[derive(Debug, PartialEq, Eq)]
8888
pub struct ResponseHeaders {
89-
content_length: i32,
89+
content_length: Option<i32>,
9090
content_type: MediaType,
9191
deprecation: bool,
9292
server: String,
@@ -151,15 +151,15 @@ impl ResponseHeaders {
151151
self.write_allow_header(buf)?;
152152
self.write_deprecation_header(buf)?;
153153

154-
if self.content_length != 0 {
154+
if let Some(content_length) = self.content_length {
155155
buf.write_all(Header::ContentType.raw())?;
156156
buf.write_all(&[COLON, SP])?;
157157
buf.write_all(self.content_type.as_str().as_bytes())?;
158158
buf.write_all(&[CR, LF])?;
159159

160160
buf.write_all(Header::ContentLength.raw())?;
161161
buf.write_all(&[COLON, SP])?;
162-
buf.write_all(self.content_length.to_string().as_bytes())?;
162+
buf.write_all(content_length.to_string().as_bytes())?;
163163
buf.write_all(&[CR, LF])?;
164164

165165
if self.accept_encoding {
@@ -173,8 +173,8 @@ impl ResponseHeaders {
173173
buf.write_all(&[CR, LF])
174174
}
175175

176-
// Sets the content length to be written in the HTTP response.
177-
fn set_content_length(&mut self, content_length: i32) {
176+
/// Sets the content length to be written in the HTTP response.
177+
pub fn set_content_length(&mut self, content_length: Option<i32>) {
178178
self.content_length = content_length;
179179
}
180180

@@ -217,10 +217,39 @@ pub struct Response {
217217

218218
impl Response {
219219
/// Creates a new HTTP `Response` with an empty body.
220+
///
221+
/// Although there are several cases where Content-Length field must not
222+
/// be sent, micro-http omits Content-Length field when the response
223+
/// status code is 1XX or 204. If needed, users can remove it by calling
224+
/// `set_content_length(None)`.
225+
///
226+
/// https://datatracker.ietf.org/doc/html/rfc9110#name-content-length
227+
/// > A server MAY send a Content-Length header field in a response to a
228+
/// > HEAD request (Section 9.3.2); a server MUST NOT send Content-Length
229+
/// > in such a response unless its field value equals the decimal number
230+
/// > of octets that would have been sent in the content of a response if
231+
/// > the same request had used the GET method.
232+
/// >
233+
/// > A server MAY send a Content-Length header field in a 304 (Not
234+
/// > Modified) response to a conditional GET request (Section 15.4.5); a
235+
/// > server MUST NOT send Content-Length in such a response unless its
236+
/// > field value equals the decimal number of octets that would have been
237+
/// > sent in the content of a 200 (OK) response to the same request.
238+
/// >
239+
/// > A server MUST NOT send a Content-Length header field in any response
240+
/// > with a status code of 1xx (Informational) or 204 (No Content). A
241+
/// > server MUST NOT send a Content-Length header field in any 2xx
242+
/// > (Successful) response to a CONNECT request (Section 9.3.6).
220243
pub fn new(http_version: Version, status_code: StatusCode) -> Self {
221244
Self {
222245
status_line: StatusLine::new(http_version, status_code),
223-
headers: ResponseHeaders::default(),
246+
headers: ResponseHeaders {
247+
content_length: match status_code {
248+
StatusCode::Continue | StatusCode::NoContent => None,
249+
_ => Some(0),
250+
},
251+
..Default::default()
252+
},
224253
body: Default::default(),
225254
}
226255
}
@@ -230,10 +259,18 @@ impl Response {
230259
/// This function has side effects because it also updates the headers:
231260
/// - `ContentLength`: this is set to the length of the specified body.
232261
pub fn set_body(&mut self, body: Body) {
233-
self.headers.set_content_length(body.len() as i32);
262+
self.headers.set_content_length(Some(body.len() as i32));
234263
self.body = Some(body);
235264
}
236265

266+
/// Updates the content length of the `Response`.
267+
///
268+
/// It is recommended to use this method only when removing Content-Length
269+
/// field if the response status is not 1XX or 204.
270+
pub fn set_content_length(&mut self, content_length: Option<i32>) {
271+
self.headers.set_content_length(content_length);
272+
}
273+
237274
/// Updates the content type of the `Response`.
238275
pub fn set_content_type(&mut self, content_type: MediaType) {
239276
self.headers.set_content_type(content_type);
@@ -296,7 +333,7 @@ impl Response {
296333

297334
/// Returns the Content Length of the response.
298335
pub fn content_length(&self) -> i32 {
299-
self.headers.content_length
336+
self.headers.content_length.unwrap_or(0)
300337
}
301338

302339
/// Returns the Content Type of the response.
@@ -359,8 +396,10 @@ mod tests {
359396
let expected_response: &'static [u8] = b"HTTP/1.0 200 \r\n\
360397
Server: Firecracker API\r\n\
361398
Connection: keep-alive\r\n\
362-
Allow: GET, PATCH, PUT\r\n\r\n";
363-
let mut response_buf: [u8; 90] = [0; 90];
399+
Allow: GET, PATCH, PUT\r\n\
400+
Content-Type: application/json\r\n\
401+
Content-Length: 0\r\n\r\n";
402+
let mut response_buf: [u8; 141] = [0; 141];
364403
assert!(response.write_all(&mut response_buf.as_mut()).is_ok());
365404
assert_eq!(response_buf.as_ref(), expected_response);
366405

@@ -480,4 +519,35 @@ mod tests {
480519
let another_response = Response::new(Version::Http10, StatusCode::BadRequest);
481520
assert_ne!(response, another_response);
482521
}
522+
523+
#[test]
524+
fn test_content_length() {
525+
// If the status code is 1XX or 204, Content-Length field must not exist.
526+
let response = Response::new(Version::Http10, StatusCode::Continue);
527+
let expected_response: &'static [u8] = b"HTTP/1.0 100 \r\n\
528+
Server: Firecracker API\r\n\
529+
Connection: keep-alive\r\n\r\n";
530+
let mut response_buf: [u8; 66] = [0; 66];
531+
assert!(response.write_all(&mut response_buf.as_mut()).is_ok());
532+
assert_eq!(response_buf.as_ref(), expected_response);
533+
534+
let response = Response::new(Version::Http10, StatusCode::NoContent);
535+
let expected_response: &'static [u8] = b"HTTP/1.0 204 \r\n\
536+
Server: Firecracker API\r\n\
537+
Connection: keep-alive\r\n\r\n";
538+
let mut response_buf: [u8; 66] = [0; 66];
539+
assert!(response.write_all(&mut response_buf.as_mut()).is_ok());
540+
assert_eq!(response_buf.as_ref(), expected_response);
541+
542+
// If not 1XX or 204, Content-Length field must exist even if the body isn't set.
543+
let response = Response::new(Version::Http10, StatusCode::OK);
544+
let expected_response: &'static [u8] = b"HTTP/1.0 200 \r\n\
545+
Server: Firecracker API\r\n\
546+
Connection: keep-alive\r\n\
547+
Content-Type: application/json\r\n\
548+
Content-Length: 0\r\n\r\n";
549+
let mut response_buf: [u8; 117] = [0; 117];
550+
assert!(response.write_all(&mut response_buf.as_mut()).is_ok());
551+
assert_eq!(response_buf.as_ref(), expected_response);
552+
}
483553
}

0 commit comments

Comments
 (0)