Skip to content
Open
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
82 changes: 82 additions & 0 deletions pingora-core/src/protocols/http/v1/body.rs
Original file line number Diff line number Diff line change
Expand Up @@ -1246,6 +1246,15 @@ impl BodyWriter {
BM::ChunkedEncoding(written) => {
let chunk_size = buf.len();

// A zero-length chunk encodes as "0\r\n\r\n", which is the
// chunked terminator: the peer would treat it as end of body
// and parse whatever follows (including the terminator that
// finish() writes) as the start of the next message. Skip it;
// finish() is the only place that ends the body.
if chunk_size == 0 {
return Ok(Some(0));
}

let chuck_size_buf = format!("{:X}\r\n", chunk_size);
let mut output_buf = Bytes::from(chuck_size_buf).chain(buf).chain(&b"\r\n"[..]);
stream
Expand Down Expand Up @@ -1737,6 +1746,15 @@ impl BodyWriter {
if let Some(bytes) = self.send_body_state.pending_bytes.take() {
let application_bytes_size = bytes.len();

// A zero-length chunk encodes as "0\r\n\r\n", the chunked
// terminator: writing it here would end the body early and
// desync the connection (see do_write_chunked_body). Skip it;
// only the finish path writes the terminator.
if application_bytes_size == 0 {
self.send_body_state.write_state = WriteState::Done(0);
return Poll::Ready(Ok(Some(0)));
}

// Format the chunk: size\r\ndata\r\n
let chunk_size_header = format!("{:X}\r\n", application_bytes_size);
let output_buf = Bytes::from(chunk_size_header)
Expand Down Expand Up @@ -3084,6 +3102,70 @@ mod tests {
assert_eq!(body_writer.body_mode, BodyMode::Complete(data.len() * 2));
}

#[tokio::test]
async fn write_body_chunked_ignores_empty_chunk() {
init_log();
let data = b"abcdefghij";
let output = b"A\r\nabcdefghij\r\n";
// The mock expects no write between the data chunk and finish():
// an empty chunk would encode as "0\r\n\r\n" — the terminator —
// ending the body early and desyncing the peer's parser.
let mut mock_io = Builder::new()
.write(&output[..])
.write(&LAST_CHUNK[..])
.build();
let mut body_writer = BodyWriter::new();
body_writer.init_chunked();
let res = body_writer
.write_body(&mut mock_io, &data[..])
.await
.unwrap()
.unwrap();
assert_eq!(res, data.len());
let res = body_writer
.write_body(&mut mock_io, b"")
.await
.unwrap()
.unwrap();
assert_eq!(res, 0);
assert_eq!(body_writer.body_mode, BodyMode::ChunkedEncoding(data.len()));
let res = body_writer.finish(&mut mock_io).await.unwrap().unwrap();
assert_eq!(res, data.len());
assert_eq!(body_writer.body_mode, BodyMode::Complete(data.len()));
}

#[tokio::test]
async fn write_body_task_chunked_ignores_empty_chunk() {
init_log();
let data = b"abcdefghij";
let output = b"A\r\nabcdefghij\r\n";
let mut mock_io = Builder::new()
.write(&output[..])
.write(&LAST_CHUNK[..])
.build();
let mut body_writer = BodyWriter::new();
body_writer.init_chunked();
body_writer.send_body_task(Bytes::from_static(data), None);
let res = body_writer
.write_current_body_task(&mut mock_io)
.await
.unwrap()
.unwrap();
assert_eq!(res, data.len());
// an empty task must complete without emitting the terminator
body_writer.send_body_task(Bytes::new(), None);
let res = body_writer
.write_current_body_task(&mut mock_io)
.await
.unwrap()
.unwrap();
assert_eq!(res, 0);
assert_eq!(body_writer.body_mode, BodyMode::ChunkedEncoding(data.len()));
let res = body_writer.finish(&mut mock_io).await.unwrap().unwrap();
assert_eq!(res, data.len());
assert_eq!(body_writer.body_mode, BodyMode::Complete(data.len()));
}

#[tokio::test]
async fn write_body_until_close() {
init_log();
Expand Down
Loading