diff --git a/proxy/hdrs/HTTP.h b/proxy/hdrs/HTTP.h index 30fcf6c9ae5..c7c43e14b98 100644 --- a/proxy/hdrs/HTTP.h +++ b/proxy/hdrs/HTTP.h @@ -623,7 +623,7 @@ class HTTPHdr : public MIMEHdr /// header internals, they must be able to do this. void mark_target_dirty() const; - HTTPStatus status_get(); + HTTPStatus status_get() const; void status_set(HTTPStatus status); const char *reason_get(int *length); @@ -642,6 +642,7 @@ class HTTPHdr : public MIMEHdr bool is_cache_control_set(const char *cc_directive_wks); bool is_pragma_no_cache_set(); bool is_keep_alive_set() const; + bool expect_final_response() const; HTTPKeepAlive keep_alive_get() const; protected: @@ -1001,6 +1002,24 @@ HTTPHdr::is_keep_alive_set() const return this->keep_alive_get() == HTTP_KEEPALIVE; } +/** + Check the status code is informational and expecting final response + - e.g. "100 Continue", "103 Early Hints" + + Please note that "101 Switching Protocol" is not included. + */ +inline bool +HTTPHdr::expect_final_response() const +{ + switch (this->status_get()) { + case HTTP_STATUS_CONTINUE: + case HTTP_STATUS_EARLY_HINTS: + return true; + default: + return false; + } +} + /*------------------------------------------------------------------------- -------------------------------------------------------------------------*/ @@ -1151,7 +1170,7 @@ http_hdr_status_get(HTTPHdrImpl *hh) -------------------------------------------------------------------------*/ inline HTTPStatus -HTTPHdr::status_get() +HTTPHdr::status_get() const { ink_assert(valid()); diff --git a/proxy/http2/Http2ConnectionState.cc b/proxy/http2/Http2ConnectionState.cc index b23e0a53e0f..3e40f1875f6 100644 --- a/proxy/http2/Http2ConnectionState.cc +++ b/proxy/http2/Http2ConnectionState.cc @@ -1416,7 +1416,7 @@ Http2ConnectionState::send_data_frames_depends_on_priority() switch (result) { case Http2SendDataFrameResult::NO_ERROR: { // No response body to send - if (len == 0 && !stream->is_body_done()) { + if (len == 0 && !stream->is_write_vio_done()) { dependency_tree->deactivate(node, len); } else { dependency_tree->update(node, len); @@ -1477,12 +1477,12 @@ Http2ConnectionState::send_a_data_frame(Http2Stream *stream, size_t &payload_len // Are we at the end? // If we return here, we never send the END_STREAM in the case of a early terminating OS. // OK if there is no body yet. Otherwise continue on to send a DATA frame and delete the stream - if (!stream->is_body_done() && payload_length == 0) { + if (!stream->is_write_vio_done() && payload_length == 0) { Http2StreamDebug(this->ua_session, stream->get_id(), "No payload"); return Http2SendDataFrameResult::NO_PAYLOAD; } - if (stream->is_body_done() && !_sm->is_read_avail_more_than(0)) { + if (stream->is_write_vio_done() && !_sm->is_read_avail_more_than(0)) { flags |= HTTP2_FLAGS_DATA_END_STREAM; } @@ -1506,7 +1506,7 @@ Http2ConnectionState::send_a_data_frame(Http2Stream *stream, size_t &payload_len this->ua_session->handleEvent(HTTP2_SESSION_EVENT_XMIT, &data); if (flags & HTTP2_FLAGS_DATA_END_STREAM) { - Http2StreamDebug(ua_session, stream->get_id(), "End of DATA frame"); + Http2StreamDebug(ua_session, stream->get_id(), "END_STREAM"); stream->send_end_stream = true; // Setting to the same state shouldn't be erroneous stream->change_state(data.header().type, data.header().flags); @@ -1584,7 +1584,9 @@ Http2ConnectionState::send_headers_frame(Http2Stream *stream) if (header_blocks_size <= static_cast(BUFFER_SIZE_FOR_INDEX(buffer_size_index[HTTP2_FRAME_TYPE_HEADERS]))) { payload_length = header_blocks_size; flags |= HTTP2_FLAGS_HEADERS_END_HEADERS; - if (h2_hdr.presence(MIME_PRESENCE_CONTENT_LENGTH) && h2_hdr.get_content_length() == 0) { + if ((h2_hdr.presence(MIME_PRESENCE_CONTENT_LENGTH) && h2_hdr.get_content_length() == 0) || + (!resp_header->expect_final_response() && stream->is_write_vio_done())) { + Http2StreamDebug(ua_session, stream->get_id(), "END_STREAM"); flags |= HTTP2_FLAGS_HEADERS_END_STREAM; stream->send_end_stream = true; } diff --git a/proxy/http2/Http2Stream.cc b/proxy/http2/Http2Stream.cc index eb98c4cbf96..8da3d862782 100644 --- a/proxy/http2/Http2Stream.cc +++ b/proxy/http2/Http2Stream.cc @@ -639,6 +639,17 @@ Http2Stream::update_write_request(IOBufferReader *buf_reader, int64_t write_len, h2_proxy_ssn->connection_state.send_headers_frame(this); } + // Roll back states of response header to read final response + if (this->response_header.expect_final_response()) { + this->response_header_done = false; + response_header.destroy(); + response_header.create(HTTP_TYPE_RESPONSE); + http_parser_clear(&http_parser); + http_parser_init(&http_parser); + } + + this->signal_write_event(call_update); + if (this->response_reader->is_read_avail_more_than(0)) { this->_milestones.mark(Http2StreamMilestone::START_TX_DATA_FRAMES); this->send_response_body(call_update); diff --git a/proxy/http2/Http2Stream.h b/proxy/http2/Http2Stream.h index 6a522a8cf6f..87ea36b5cd7 100644 --- a/proxy/http2/Http2Stream.h +++ b/proxy/http2/Http2Stream.h @@ -112,7 +112,7 @@ class Http2Stream : public ProxyTransaction void increment_data_length(uint64_t length); bool payload_length_is_valid() const; - bool is_body_done() const; + bool is_write_vio_done() const; void update_sent_count(unsigned num_bytes); Http2StreamId get_id() const; Http2StreamState get_state() const; @@ -230,7 +230,7 @@ Http2Stream::mark_milestone(Http2StreamMilestone type) } inline bool -Http2Stream::is_body_done() const +Http2Stream::is_write_vio_done() const { return this->write_vio.ntodo() == 0; } diff --git a/tests/gold_tests/h2/gold/http2_9_stderr.gold b/tests/gold_tests/h2/gold/http2_9_stderr.gold new file mode 100644 index 00000000000..ea2f1297786 --- /dev/null +++ b/tests/gold_tests/h2/gold/http2_9_stderr.gold @@ -0,0 +1,10 @@ +`` +> GET /status/204 HTTP/2 +> Host: `` +> User-Agent: curl/`` +> Accept: */* +`` +< HTTP/2 204`` +< server: ATS/`` +< date: `` +`` diff --git a/tests/gold_tests/h2/gold/http2_9_stdout.gold b/tests/gold_tests/h2/gold/http2_9_stdout.gold new file mode 100644 index 00000000000..e69de29bb2d diff --git a/tests/gold_tests/h2/gold/httpbin_0_stdout.gold b/tests/gold_tests/h2/gold/httpbin_0_stdout.gold index c46139dc0bf..d8e61077a93 100644 --- a/tests/gold_tests/h2/gold/httpbin_0_stdout.gold +++ b/tests/gold_tests/h2/gold/httpbin_0_stdout.gold @@ -1,10 +1,5 @@ { - "args": {}, `` - "headers": { - "Accept": "*/*", `` - "Host": `` - "User-Agent": "curl/`` - }, `` - "origin": `` + `` "url": "http://``/get" + `` } diff --git a/tests/gold_tests/h2/gold/httpbin_3_stderr.gold b/tests/gold_tests/h2/gold/httpbin_3_stderr.gold new file mode 100644 index 00000000000..8109f9de4e4 --- /dev/null +++ b/tests/gold_tests/h2/gold/httpbin_3_stderr.gold @@ -0,0 +1,9 @@ +`` +> POST /post HTTP/2 +`` +> Expect: 100-continue +`` +< HTTP/2 100`` +`` +< HTTP/2 200`` +`` diff --git a/tests/gold_tests/h2/gold/httpbin_3_stdout.gold b/tests/gold_tests/h2/gold/httpbin_3_stdout.gold new file mode 100644 index 00000000000..610580810ce --- /dev/null +++ b/tests/gold_tests/h2/gold/httpbin_3_stdout.gold @@ -0,0 +1,7 @@ +{ + `` + "form": { + "key": "value" + }, + `` +} diff --git a/tests/gold_tests/h2/gold/httpbin_access.gold b/tests/gold_tests/h2/gold/httpbin_access.gold index 3f77947edb3..d409c47d13a 100644 --- a/tests/gold_tests/h2/gold/httpbin_access.gold +++ b/tests/gold_tests/h2/gold/httpbin_access.gold @@ -1,3 +1,4 @@ [``] GET http://127.0.0.1:``/get HTTP/1.1 http/2 `` `` TCP_MISS 200 `` [``] GET http://127.0.0.1:``/bytes/0 HTTP/1.1 http/2 `` `` TCP_MISS 200 0 [``] GET http://127.0.0.1:``/stream-bytes/102400?seed=0 HTTP/1.1 http/2 `` `` TCP_MISS 200 102400 +`` diff --git a/tests/gold_tests/h2/http2.test.py b/tests/gold_tests/h2/http2.test.py index 54842726165..c0ff4b3706e 100644 --- a/tests/gold_tests/h2/http2.test.py +++ b/tests/gold_tests/h2/http2.test.py @@ -69,6 +69,11 @@ {"headers": "GET /huge_resp_hdrs HTTP/1.1\r\nHost: www.example.com\r\n\r\n", "timestamp": "1469733493.993", "body": ""}, {"headers": "HTTP/1.1 200 OK\r\nServer: microserver\r\nConnection: close\r\nContent-Length: 6\r\n\r\n", "timestamp": "1469733493.993", "body": "200 OK"}) +# For Test Case 9 - /status/204 +server.addResponse("sessionlog.json", + {"headers": "GET /status/204 HTTP/1.1\r\nHost: www.example.com\r\n\r\n", "timestamp": "1469733493.993", "body": ""}, + {"headers": "HTTP/1.1 204 No Content\r\nServer: microserver\r\nConnection: close\r\n\r\n", "timestamp": "1469733493.993", "body": ""}) + # ---- # Setup ATS # ---- @@ -177,3 +182,11 @@ tr.Processes.Default.Streams.stdout = "gold/http2_8_stdout.gold" tr.Processes.Default.Streams.stderr = "gold/http2_8_stderr.gold" tr.StillRunningAfter = server + +# Test Case 9: Header Only Response - e.g. 204 +tr = Test.AddTestRun() +tr.Processes.Default.Command = 'curl -vs -k --http2 https://127.0.0.1:{0}/status/204'.format(ts.Variables.ssl_port) +tr.Processes.Default.ReturnCode = 0 +tr.Processes.Default.Streams.stdout = "gold/http2_9_stdout.gold" +tr.Processes.Default.Streams.stderr = "gold/http2_9_stderr.gold" +tr.StillRunningAfter = server diff --git a/tests/gold_tests/h2/httpbin.test.py b/tests/gold_tests/h2/httpbin.test.py index d31247c5b09..d77fcc282be 100644 --- a/tests/gold_tests/h2/httpbin.test.py +++ b/tests/gold_tests/h2/httpbin.test.py @@ -78,14 +78,19 @@ Test.Disk.File(os.path.join(ts.Variables.LOGDIR, 'access.log'), exists=True, content='gold/httpbin_access.gold') +# TODO: when httpbin 0.8.0 or later is released, remove below json pretty print hack +json_printer = ''' +python -c "import sys,json; print(json.dumps(json.load(sys.stdin), indent=2))" +''' + # ---- # Test Cases # ---- # Test Case 0: Basic request and resposne test_run = Test.AddTestRun() -# TODO: when httpbin 0.8.0 or later is released, remove below json pretty print hack -test_run.Processes.Default.Command = "curl -vs -k --http2 https://127.0.0.1:{0}/get | python -c 'import sys,json; print(json.dumps(json.load(sys.stdin), indent=2))".format(ts.Variables.ssl_port) +test_run.Processes.Default.Command = "curl -vs -k --http2 https://127.0.0.1:{0}/get | {1}".format( + ts.Variables.ssl_port, json_printer) test_run.Processes.Default.ReturnCode = 0 test_run.Processes.Default.StartBefore(httpbin, ready=When.PortOpen(httpbin.Variables.Port)) test_run.Processes.Default.StartBefore(Test.Processes.ts) @@ -110,6 +115,16 @@ test_run.Processes.Default.Streams.stderr = "gold/httpbin_2_stderr.gold" test_run.StillRunningAfter = httpbin +# Test Case 3: Expect 100-Continue +test_run = Test.AddTestRun() +test_run.Processes.Default.Command = "curl -vs -k --http2 https://127.0.0.1:{0}/post --data 'key=value' -H 'Expect: 100-continue' --expect100-timeout 1 --max-time 5 | {1}".format( + ts.Variables.ssl_port, json_printer) +test_run.Processes.Default.ReturnCode = 0 +test_run.Processes.Default.Streams.stdout = "gold/httpbin_3_stdout.gold" +test_run.Processes.Default.Streams.stderr = "gold/httpbin_3_stderr.gold" +test_run.StillRunningAfter = httpbin + + # Check Logging test_run = Test.AddTestRun() test_run.DelayStart = 10