-
Notifications
You must be signed in to change notification settings - Fork 854
Fix 408 timeout caused by ignoring DATA frames after GOAWAY #12936
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: master
Are you sure you want to change the base?
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change | ||
|---|---|---|---|---|
|
|
@@ -118,6 +118,14 @@ Http2ConnectionState::rcv_data_frame(const Http2Frame &frame) | |||
| "recv data bad frame client id"); | ||||
| } | ||||
|
|
||||
| // After sending a GOAWAY frame, the sender can discard frames for streams initiated by the receiver with | ||||
| // identifiers higher than the identified last stream. However, DATA frames MUST be counted toward | ||||
| // the connection flow-control window. (Details in [RFC 9113] 6.8.) | ||||
| if (this->goaway_sent && id > this->last_stream_id_tx) { | ||||
| return Http2Error(Http2ErrorClass::HTTP2_ERROR_CLASS_STREAM, Http2ErrorCode::HTTP2_ERROR_REFUSED_STREAM, | ||||
| "recv data with id higher than last stream id"); | ||||
| } | ||||
|
|
||||
| Http2Stream *stream = this->find_stream(id); | ||||
| if (stream == nullptr) { | ||||
| if (this->is_valid_streamid(id)) { | ||||
|
|
@@ -330,15 +338,25 @@ Http2ConnectionState::rcv_headers_frame(const Http2Frame &frame) | |||
| "recv headers bad client id"); | ||||
| } | ||||
|
|
||||
| // After sending a GOAWAY frame, the sender can discard frames for streams initiated by the receiver with | ||||
| // identifiers higher than the identified last stream. However, HEADERS, PUSH_PROMISE, and CONTINUATION | ||||
| // frames MUST be minimally processed to ensure that the state maintained for field section compression is | ||||
| // consistent (Details in [RFC 9113] 6.8.) | ||||
| if (this->goaway_sent && stream_id > this->last_stream_id_tx) { | ||||
| reset_header_after_decoding = true; | ||||
| } | ||||
|
|
||||
| if (!stream) { | ||||
| if (reset_header_after_decoding) { | ||||
| free_stream_after_decoding = true; | ||||
| uint32_t const initial_local_stream_window = this->acknowledged_local_settings.get(HTTP2_SETTINGS_INITIAL_WINDOW_SIZE); | ||||
| ink_assert(dynamic_cast<Http2CommonSession *>(this->session->get_proxy_session())); | ||||
| ink_assert(this->session->is_outbound() == true); | ||||
| stream = THREAD_ALLOC_INIT(http2StreamAllocator, this_ethread(), this->session->get_proxy_session(), stream_id, | ||||
| this->peer_settings.get(HTTP2_SETTINGS_INITIAL_WINDOW_SIZE), initial_local_stream_window, | ||||
| !STREAM_IS_REGISTERED); | ||||
| stream = THREAD_ALLOC_INIT(http2StreamAllocator, this_ethread(), this->session->get_proxy_session(), stream_id, | ||||
| this->peer_settings.get(HTTP2_SETTINGS_INITIAL_WINDOW_SIZE), initial_local_stream_window, | ||||
| !STREAM_IS_REGISTERED); | ||||
| stream->mutex = new_ProxyMutex(); | ||||
| SCOPED_MUTEX_LOCK(lock, this->mutex, this_ethread()); | ||||
| this->stream_list.enqueue(stream); | ||||
| } else { | ||||
| // Create new stream | ||||
| Http2Error error(Http2ErrorClass::HTTP2_ERROR_CLASS_NONE); | ||||
|
|
@@ -370,6 +388,9 @@ Http2ConnectionState::rcv_headers_frame(const Http2Frame &frame) | |||
| } | ||||
| } | ||||
|
|
||||
| stream->reset_header_after_decoding = reset_header_after_decoding; | ||||
| stream->free_stream_after_decoding = free_stream_after_decoding; | ||||
|
|
||||
| Http2HeadersParameter params; | ||||
| uint32_t header_block_fragment_offset = 0; | ||||
| uint32_t header_block_fragment_length = payload_length; | ||||
|
|
@@ -477,13 +498,21 @@ Http2ConnectionState::rcv_headers_frame(const Http2Frame &frame) | |||
| Http2ErrorCode result = stream->decode_header_blocks(*this->local_hpack_handle, | ||||
| this->acknowledged_local_settings.get(HTTP2_SETTINGS_HEADER_TABLE_SIZE)); | ||||
|
|
||||
| // If this was an outbound connection and the state was already closed, just clear the | ||||
| // headers after processing. We just processed the heaer blocks to keep the dynamic table in | ||||
| // We just processed the header blocks to keep the dynamic table in | ||||
| // sync with peer to avoid future HPACK compression errors | ||||
| if (reset_header_after_decoding) { | ||||
| stream->reset_receive_headers(); | ||||
| SCOPED_MUTEX_LOCK(lock, this->mutex, this_ethread()); | ||||
| this->stream_list.remove(stream); | ||||
| if (free_stream_after_decoding) { | ||||
| THREAD_FREE(stream, http2StreamAllocator, this_ethread()); | ||||
| // Suppress RST_STREAM(NO_ERROR): rcv_frame() will send RST_STREAM(REFUSED_STREAM) | ||||
| // for this stream, so sending NO_ERROR here would produce duplicate frames. | ||||
| stream->initiating_close(/* suppress_rst= */ true); | ||||
| } | ||||
|
Comment on lines
507
to
+511
|
||||
| ink_release_assert(this->closed); |
Regarding the alternative approach of modifying the stream state before calling initiating_close, this relies on the assumption that an RST_STREAM frame will be sent later in the subsequent processing flow. If that assumption changes in the future, it could introduce another issue. Therefore, I decided not to adopt that approach.
Copilot
AI
Mar 9, 2026
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Same issue as in rcv_headers_frame: initiating_close() may send an RST_STREAM(NO_ERROR) if the stream is in a writeable state, and then the HTTP2_ERROR_CLASS_STREAM return at line 1144 causes rcv_frame() to send a second RST_STREAM(REFUSED_STREAM). This results in duplicate RST_STREAM frames for the same stream.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This PR fixes a significant behavioral issue (408 timeouts caused by ignoring DATA frames after GOAWAY), but doesn't include any test coverage for the new behavior. Given the existing test infrastructure in
tests/gold_tests/h2/and the complexity of this fix (continuing to process frames after GOAWAY, minimal HPACK processing for HEADERS/CONTINUATION on refused streams, flow-control counting for DATA), it would be valuable to add an integration test that validates the core scenario: sending multiple POST requests over a single HTTP/2 connection where the stream error rate threshold is exceeded, and verifying that in-flight requests with pending DATA frames complete successfully rather than timing out.