Skip to content
Open
Show file tree
Hide file tree
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
10 changes: 10 additions & 0 deletions parser/src/parse.rs
Original file line number Diff line number Diff line change
Expand Up @@ -485,6 +485,16 @@ impl Parser {
}
}

// Reserve the whole header's event batch before validating or
// committing any semantic effects. Otherwise a full event buffer
// can suspend between the name and value events after state such
// as Content-Length has already been updated.
let header_events_size =
(has_header_name_event as usize + has_header_value_event as usize) * EVENT_RANGE_SIZE;
if event_cursor + header_events_size + EVENT_ERROR_RESERVE > EVENTS_BUFFER_SIZE {
suspend!();
}

// RFC 9112 section.4
// RFC 9110 section 5.5 and 5.6
let header_name_start = 0;
Expand Down
29 changes: 29 additions & 0 deletions parser/tests/basic.rs
Original file line number Diff line number Diff line change
Expand Up @@ -415,6 +415,35 @@ fn basic_event_buffer_full_stops_parsing() {
assert_eq!(parser.error_code, ERROR_NONE);
}

#[test]
fn basic_event_buffer_suspension_preserves_header_atomicity() {
for framing in [
"Content-Length: 1\r\n\r\nx",
"Transfer-Encoding: chunked\r\n\r\n0\r\n\r\n",
] {
let mut parser = milo_parser::Parser::new();
parser.autodetect = false;
parser.is_request = true;
parser.active_events = EVENT_ACTIVE_ON_HEADER_NAME | EVENT_ACTIVE_ON_HEADER_VALUE;

// Two range events use 18 bytes per header. This fills the 64 KiB event
// buffer so the next name fits but its value does not.
let message = format!("POST / HTTP/1.1\r\n{}{}", "X:a\r\n".repeat(3640), framing);
let consumed = parser.parse(message.as_ptr(), message.len());

assert!(consumed < message.len());
assert!(!parser.has_content_length);
assert!(!parser.has_transfer_encoding);
assert!(!parser.has_chunked_transfer_encoding);
assert_eq!(parser.error_code, ERROR_NONE);

let remaining = &message.as_bytes()[consumed..];
assert_eq!(parser.parse(remaining.as_ptr(), remaining.len()), remaining.len());
assert_eq!(parser.state, STATE_START);
assert_eq!(parser.error_code, ERROR_NONE);
}
}

#[test]
fn basic_sample_multiple_responses() {
let mut parser = create_parser();
Expand Down
26 changes: 26 additions & 0 deletions parser/wasm/test/basic.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -279,6 +279,32 @@ it('basic_event_buffer_full_stops_parsing', t => {
assert.equal(milo.getErrorCode(parser), milo.ERROR_NONE)
})

it('basic_event_buffer_suspension_preserves_header_atomicity', t => {
for (const framing of ['Content-Length: 1\r\n\r\nx', 'Transfer-Encoding: chunked\r\n\r\n0\r\n\r\n']) {
const { milo, parser, parse } = createParser(t, setup)
milo.setShouldAutodetect(parser, false)
milo.setIsRequest(parser, true)
milo.setActiveCallbacks(parser, 0n)
milo.setActiveEvents(parser, milo.EVENT_ACTIVE_ON_HEADER_NAME | milo.EVENT_ACTIVE_ON_HEADER_VALUE)

// Two range events use 18 bytes per header. This fills the 64 KiB event
// buffer so the next name fits but its value does not.
const message = `POST / HTTP/1.1\r\n${'X:a\r\n'.repeat(3640)}${framing}`
const consumed = parse(message)

assert.ok(consumed < message.length)
assert.equal(milo.hasContentLength(parser), false)
assert.equal(milo.hasTransferEncoding(parser), false)
assert.equal(milo.hasChunkedTransferEncoding(parser), false)
assert.equal(milo.getErrorCode(parser), milo.ERROR_NONE)

const remaining = message.slice(consumed)
assert.equal(parse(remaining), remaining.length)
assert.equal(milo.getState(parser), milo.STATE_START)
assert.equal(milo.getErrorCode(parser), milo.ERROR_NONE)
}
})

it('basic_event_buffer_full_resumes_completion', () => {
for (const close of [false, true]) {
const received = []
Expand Down
Loading