Skip to content

Commit aaff427

Browse files
beekldkinyoklion
andauthored
Apply suggestion from @kinyoklion
Co-authored-by: Ryan Lamb <4955475+kinyoklion@users.noreply.github.com>
1 parent 0659239 commit aaff427

1 file changed

Lines changed: 35 additions & 4 deletions

File tree

libs/server-sent-events/src/curl_client.cpp

Lines changed: 35 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -437,12 +437,32 @@ size_t CurlClient::HeaderCallback(char const* buffer,
437437
// End-of-headers terminator: emit and reset.
438438
context->response(std::move(context->current_response));
439439
context->current_response = http::response_header<>{};
440+
// Strip the line terminator. Allow bare LF or bare CR per RFC 9112 §2.2;
441+
// libcurl preserves the original wire bytes for HTTP/1.x (only HTTP/2
442+
// synthesizes CRLF), so a non-compliant origin can deliver bare LF here.
443+
while (!line.empty() &&
444+
(line.back() == '\r' || line.back() == '\n')) {
445+
line.remove_suffix(1);
446+
}
447+
448+
if (line.empty()) {
449+
// Terminator. If we're between responses (e.g., the line ends a
450+
// chunked-transfer trailer block), there's nothing to emit.
451+
if (context->reading_headers) {
452+
context->response(std::move(context->current_response));
453+
context->current_response = http::response_header<>{};
454+
context->reading_headers = false;
455+
}
440456
return total_size;
441457
}
442458

443459
if (line.substr(0, 5) == "HTTP/") {
444-
// Status line: "HTTP/X.Y CODE REASON". Start a fresh response.
445-
// Status line: "HTTP/X.Y CODE REASON". Start a fresh response.
460+
// Status line: "HTTP/X.Y CODE REASON". Only legitimate before any
461+
// header has been seen for this response — an interior HTTP/ line
462+
// would otherwise wipe accumulated state.
463+
if (context->reading_headers) {
464+
return total_size;
465+
}
446466
// Beast default-constructs result_ to status::ok (200); reset to 0
447467
// so an unparseable status line surfaces as result_int() == 0.
448468
context->current_response = http::response_header<>{};
@@ -452,10 +472,19 @@ size_t CurlClient::HeaderCallback(char const* buffer,
452472
unsigned code = 0;
453473
auto const result = std::from_chars(
454474
line.data() + code_start + 1, line.data() + line.size(), code);
455-
if (result.ec == std::errc{} && code != 0) {
475+
// Three-digit status per RFC 7231 §6; the tight bound avoids
476+
// result(unsigned) throwing across the libcurl C frame.
477+
if (result.ec == std::errc{} && code >= 100 && code <= 999) {
456478
context->current_response.result(code);
457479
}
458480
}
481+
context->reading_headers = true;
482+
return total_size;
483+
}
484+
485+
if (!context->reading_headers) {
486+
// Header line received outside an active response — chunked trailer
487+
// or protocol-level junk. Ignore.
459488
return total_size;
460489
}
461490

@@ -472,7 +501,9 @@ size_t CurlClient::HeaderCallback(char const* buffer,
472501
(value.back() == ' ' || value.back() == '\t')) {
473502
value.remove_suffix(1);
474503
}
475-
context->current_response.set(std::string(name), std::string(value));
504+
// insert() preserves duplicate-name headers (Set-Cookie, Via, …);
505+
// set() would collapse them and diverge from the Foxy backend.
506+
context->current_response.insert(std::string(name), std::string(value));
476507

477508
if (beast::iequals(name, "Content-Type") &&
478509
value.find("text/event-stream") == std::string_view::npos) {

0 commit comments

Comments
 (0)