Skip to content

Commit 7ef550c

Browse files
authored
HTTP/2 expect: 100-continue updates. (#308)
1 parent b15056c commit 7ef550c

File tree

6 files changed

+120
-51
lines changed

6 files changed

+120
-51
lines changed

local/include/core/http.h

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -167,6 +167,7 @@ static constexpr swoc::TextView HTTP_EOL{"\r\n"};
167167
static constexpr swoc::TextView HTTP_EOH{"\r\n\r\n"};
168168

169169
class HttpHeader;
170+
class HttpFields;
170171
class RuleCheck;
171172
struct Txn;
172173
class ProxyProtocolMsg;
@@ -178,8 +179,22 @@ namespace swoc
178179
{
179180
inline namespace SWOC_VERSION_NS
180181
{
182+
/** Print the HTTP request or response headers.
183+
*
184+
* @param[out] w The BufferWriter to write to.
185+
* @param[in] spec Format specifier for output.
186+
* @param[in] h The HttpHeader to print out.
187+
*/
181188
BufferWriter &bwformat(BufferWriter &w, bwf::Spec const &spec, HttpHeader const &h);
182189

190+
/** Print the fields sequence.
191+
*
192+
* @param[out] w The BufferWriter to write to.
193+
* @param[in] spec Format specifier for output.
194+
* @param[in] f The HttpFields to print out.
195+
*/
196+
BufferWriter &bwformat(BufferWriter &w, bwf::Spec const &spec, HttpFields const &f);
197+
183198
/** Formatter for ProxyProtocolMsg which pretty-prints the proxy protocol in
184199
* the human-readable v1 format
185200
* @param[out] w The BufferWriter to write to.

local/include/core/http2.h

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -65,6 +65,7 @@ class H2StreamState
6565
size_t _send_body_offset = 0;
6666
bool _wait_for_continue = false;
6767
bool _last_data_frame = false;
68+
bool _wait_for_response_after_100_continue = false;
6869
std::string _key;
6970

7071
nghttp2_nv *_trailer_to_send = nullptr;

local/src/core/http.cc

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -79,6 +79,22 @@ bwformat(BufferWriter &w, bwf::Spec const & /* spec */, HttpHeader const &h)
7979
}
8080
w.print(R"({}: {}{})", key, value, '\n');
8181
}
82+
if (!h._trailer_fields_rules->_fields_sequence.empty()) {
83+
w.print("----------------\n");
84+
w.print("Trailer Headers:\n");
85+
w.print("----------------\n");
86+
for (auto const &[key, value] : h._trailer_fields_rules->_fields_sequence) {
87+
w.print(R"({}: {}{})", key, value, '\n');
88+
}
89+
}
90+
return w;
91+
}
92+
BufferWriter &
93+
bwformat(BufferWriter &w, bwf::Spec const & /* spec */, HttpFields const &f)
94+
{
95+
for (auto const &[key, value] : f._fields_sequence) {
96+
w.print(R"({}: {}{})", key, value, '\n');
97+
}
8298
return w;
8399
}
84100
// custom formatting for the proxy header

local/src/core/http2.cc

Lines changed: 67 additions & 49 deletions
Original file line numberDiff line numberDiff line change
@@ -438,6 +438,8 @@ on_begin_headers_callback(
438438
request_headers->_stream_id = stream_id;
439439
break;
440440
}
441+
case NGHTTP2_HCAT_HEADERS:
442+
// Final response headers after a 1xx response or trailer headers.
441443
case NGHTTP2_HCAT_RESPONSE: {
442444
auto stream_map_iter = session_data->_stream_map.find(stream_id);
443445
if (stream_map_iter == session_data->_stream_map.end()) {
@@ -454,11 +456,7 @@ on_begin_headers_callback(
454456
response_headers->_contains_pseudo_headers_in_fields_array = true;
455457
break;
456458
}
457-
case NGHTTP2_HCAT_HEADERS:
458-
// Headers that are not request or response headers. Trailer headers, for
459-
// example, falls into this category.
460-
break;
461-
default:
459+
case NGHTTP2_HCAT_PUSH_RESPONSE:
462460
errata.note(S_ERROR, "Got HTTP/2 headers for an unimplemented category: {}", headers_category);
463461
break;
464462
}
@@ -510,31 +508,31 @@ on_header_callback(
510508
break;
511509
}
512510

511+
case NGHTTP2_HCAT_HEADERS: {
512+
// This is either the response headers after a 1xx response or
513+
// trailer headers.
514+
if (!stream_state->_wait_for_response_after_100_continue) {
515+
// Must be trailer headers.
516+
auto &response_headers = stream_state->_response_from_server;
517+
response_headers->_trailer_fields_rules->add_field(name_view, value_view);
518+
break;
519+
}
520+
// If we reach here, we received the response headers after a 1xx response.
521+
// This is handled along with the NGHTTP2_HCAT_RESPONSE case.
522+
[[fallthrough]];
523+
}
513524
case NGHTTP2_HCAT_RESPONSE: {
514525
auto &response_headers = stream_state->_response_from_server;
515526
if (name_view == ":status") {
516527
response_headers->_status = swoc::svtou(value_view);
517528
response_headers->_status_string = std::string(value_view);
518529
}
519530
response_headers->_fields_rules->add_field(name_view, value_view);
520-
// See if we are expecting a 100 response.
521-
if (stream_state->_wait_for_continue) {
522-
if (name_view == ":status" && value_view == "100") {
523-
// We got our 100 Continue. No need to wait for it anymore.
524-
stream_state->_wait_for_continue = false;
525-
}
526-
}
527-
break;
528-
}
529-
case NGHTTP2_HCAT_HEADERS: {
530-
auto &response_headers = stream_state->_response_from_server;
531-
response_headers->_trailer_fields_rules->add_field(name_view, value_view);
532-
break;
533-
}
531+
} break;
534532
case NGHTTP2_HCAT_PUSH_RESPONSE:
535533
errata.note(
536534
S_ERROR,
537-
"Got HTTP/2 an header for an unimplemented category: {}",
535+
"Got an HTTP/2 header for an unimplemented category: {}",
538536
headers_category);
539537
return 0;
540538
}
@@ -850,7 +848,8 @@ on_frame_recv_cb(nghttp2_session * /* session */, nghttp2_frame const *frame, vo
850848
stream_state._key,
851849
stream_id,
852850
request_from_client);
853-
} else if (headers_category == NGHTTP2_HCAT_RESPONSE) {
851+
} else if (
852+
headers_category == NGHTTP2_HCAT_RESPONSE || headers_category == NGHTTP2_HCAT_HEADERS) {
854853
auto &response_from_wire = *stream_state._response_from_server;
855854
response_from_wire.derive_key();
856855
if (stream_state._key.empty()) {
@@ -873,43 +872,60 @@ on_frame_recv_cb(nghttp2_session * /* session */, nghttp2_frame const *frame, vo
873872
// the fields of both the request and response.
874873
response_from_wire.set_key(stream_state._key);
875874
}
875+
auto const &key = stream_state._key;
876876
if (auto spot{
877877
response_from_wire._fields_rules->_fields.find(HttpHeader::FIELD_CONTENT_LENGTH)};
878878
spot != response_from_wire._fields_rules->_fields.end())
879879
{
880880
size_t expected_size = swoc::svtou(spot->second);
881881
stream_state._body_received.reserve(expected_size);
882882
}
883-
errata.note(
884-
S_DIAG,
885-
"Received an HTTP/2 response for key {} with stream id {}:\n{}",
886-
stream_state._key,
887-
stream_id,
888-
response_from_wire);
889-
auto const &key = stream_state._key;
890-
auto const &specified_response = stream_state._specified_response;
891-
if (specified_response &&
892-
response_from_wire.verify_headers(key, *specified_response->_fields_rules))
893-
{
883+
if (headers_category == NGHTTP2_HCAT_RESPONSE) {
894884
errata.note(
895-
S_ERROR,
896-
R"(HTTP/2 response headers did not match expected response headers.)");
897-
session_data->set_non_zero_exit_status();
898-
}
899-
if (specified_response && specified_response->_status != 0 &&
900-
response_from_wire._status != specified_response->_status &&
901-
(response_from_wire._status != 200 || specified_response->_status != 304) &&
902-
(response_from_wire._status != 304 || specified_response->_status != 200))
903-
{
885+
S_DIAG,
886+
"Received an HTTP/2 response for key {} with stream id {}:\n{}",
887+
key,
888+
stream_id,
889+
response_from_wire);
890+
} else {
904891
errata.note(
905-
S_ERROR,
906-
R"(HTTP/2 Status Violation: expected {} got {}, key: {})",
907-
specified_response->_status,
908-
response_from_wire._status,
909-
key);
892+
S_DIAG,
893+
"Received HTTP/2 response trailers for key {} with stream id {}:\n{}",
894+
key,
895+
stream_id,
896+
*response_from_wire._trailer_fields_rules);
897+
}
898+
if (response_from_wire._status == 100) {
899+
// Prepare for the final response after the 100 Continue.
900+
stream_state._response_from_server = std::make_shared<HttpHeader>();
901+
stream_state._response_from_server->_stream_id = stream_id;
902+
stream_state._wait_for_continue = false;
903+
stream_state._wait_for_response_after_100_continue = true;
904+
errata.note("Received 100 Continue for key {}, now awaiting response headers", key);
905+
} else {
906+
stream_state._wait_for_response_after_100_continue = false;
907+
auto const &specified_response = stream_state._specified_response;
908+
if (specified_response &&
909+
response_from_wire.verify_headers(key, *specified_response->_fields_rules))
910+
{
911+
errata.note(
912+
S_ERROR,
913+
R"(HTTP/2 response headers did not match expected response headers.)");
914+
session_data->set_non_zero_exit_status();
915+
}
916+
if (specified_response && specified_response->_status != 0 &&
917+
response_from_wire._status != specified_response->_status &&
918+
(response_from_wire._status != 200 || specified_response->_status != 304) &&
919+
(response_from_wire._status != 304 || specified_response->_status != 200))
920+
{
921+
errata.note(
922+
S_ERROR,
923+
R"(HTTP/2 Status Violation: expected {} got {}, key: {})",
924+
specified_response->_status,
925+
response_from_wire._status,
926+
key);
927+
}
910928
}
911-
} else if (headers_category == NGHTTP2_HCAT_HEADERS) {
912-
errata.note(S_DIAG, "Received an HTTP/2 trailer for stream id {}:\n", stream_id);
913929
}
914930
}
915931
if (flags & NGHTTP2_FLAG_END_STREAM) {
@@ -1543,6 +1559,9 @@ H2Session::write(HttpHeader const &hdr)
15431559
}
15441560

15451561
stream_state->_key = hdr.get_key();
1562+
// A user shouldn't set Expect: 100-continue with a request with no body,
1563+
// but we support it anyway.
1564+
stream_state->_wait_for_continue = hdr.is_request_with_expect_100_continue();
15461565
if (hdr._content_length > 0 &&
15471566
(hdr.is_request() || !HttpHeader::STATUS_NO_CONTENT[hdr._status])) {
15481567
TextView content;
@@ -1561,7 +1580,6 @@ H2Session::write(HttpHeader const &hdr)
15611580
stream_state->_body_to_send = content.data();
15621581
stream_state->_send_body_length = content.size();
15631582
stream_state->_send_body_offset = 0;
1564-
stream_state->_wait_for_continue = hdr.is_request_with_expect_100_continue();
15651583
stream_state->_last_data_frame = true;
15661584
if (hdr.is_response()) {
15671585
// Pack the trailer headers.

test/autests/gold_tests/http2/gold/http2_to_http2_client.gold

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -123,8 +123,10 @@ uuid: 5
123123
``Received an HTTP/2 body of 16 bytes for key 5 with stream id 9:
124124
0123456789abcdef
125125
``
126-
``Received an HTTP/2 trailer for stream id 9:
127-
126+
``Received HTTP/2 response trailers for key 5 with stream id 9:
127+
x-test-trailer-1: one
128+
x-test-trailer-2: two
129+
``
128130
``Equals Success: Key: "5", Field Name: "x-test-trailer-1", Value: "one"
129131
``Equals Success: Key: "5", Field Name: "x-test-trailer-2", Value: "two"
130132
``

test/autests/gold_tests/http2/gold/http2_to_http2_server.gold

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -87,3 +87,20 @@ uuid: 4
8787
``
8888
``Received an HTTP/2 request for key 5 with stream id 9:
8989
``
90+
``headers``key 5``
91+
:status: 200
92+
cache-control: private
93+
content-type: application/json;charset=utf-8
94+
content-length: 16
95+
date: Sat, 16 Mar 2019 01:13:21 GMT
96+
age: 0
97+
uuid: 5
98+
----------------
99+
Trailer Headers:
100+
----------------
101+
x-test-trailer-1: one
102+
x-test-trailer-2: two
103+
``
104+
``body``key 5``
105+
0123456789abcdef
106+
``

0 commit comments

Comments
 (0)