Skip to content

Commit d383e95

Browse files
TingDaoKgraebm
andauthored
Receive GOAWAY (#234)
Receive GOAWAY frame: - Stop creating and sending new streams. - Mark the streams with higher ID than the last_stream_id indicated by the GOAWAY frame as completed with error to inform the user to retry them on a new connection. TODO: - Inform our user about the debug information in GOAWAY. Co-authored-by: Michael Graeb <[email protected]> Co-authored-by: Dengke Tang <[email protected]>
1 parent e684773 commit d383e95

File tree

6 files changed

+201
-0
lines changed

6 files changed

+201
-0
lines changed

include/aws/http/http.h

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -51,6 +51,7 @@ enum aws_http_errors {
5151
AWS_ERROR_HTTP_CHANNEL_THROUGHPUT_FAILURE,
5252
AWS_ERROR_HTTP_PROTOCOL_ERROR,
5353
AWS_ERROR_HTTP_STREAM_IDS_EXHAUSTED,
54+
AWS_ERROR_HTTP_GOAWAY_RECEIVED,
5455
AWS_ERROR_HTTP_RST_STREAM_RECEIVED,
5556

5657
AWS_ERROR_HTTP_END_RANGE = AWS_ERROR_ENUM_END_RANGE(AWS_C_HTTP_PACKAGE_ID)

include/aws/http/private/h2_connection.h

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -90,6 +90,11 @@ struct aws_h2_connection {
9090
* Reduce the space after receiving a flow-controlled frame. Increment after sending WINDOW_UPDATE for
9191
* connection */
9292
size_t window_size_self;
93+
94+
/* Highest self-initiated stream-id that peer might have processed.
95+
* Defaults to max stream-id, may be lowered when GOAWAY frame received. */
96+
uint32_t goaway_received_last_stream_id;
97+
9398
} thread_data;
9499

95100
/* Any thread may touch this data, but the lock must be held (unless it's an atomic) */

source/h2_connection.c

Lines changed: 68 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -55,6 +55,7 @@ static size_t s_handler_initial_window_size(struct aws_channel_handler *handler)
5555
static size_t s_handler_message_overhead(struct aws_channel_handler *handler);
5656
static void s_handler_destroy(struct aws_channel_handler *handler);
5757
static void s_handler_installed(struct aws_channel_handler *handler, struct aws_channel_slot *slot);
58+
static void s_stream_complete(struct aws_h2_connection *connection, struct aws_h2_stream *stream, int error_code);
5859
static struct aws_http_stream *s_connection_make_request(
5960
struct aws_http_connection *client_connection,
6061
const struct aws_http_make_request_options *options);
@@ -98,6 +99,11 @@ static struct aws_h2err s_decoder_on_settings(
9899
void *userdata);
99100
static struct aws_h2err s_decoder_on_settings_ack(void *userdata);
100101
static struct aws_h2err s_decoder_on_window_update(uint32_t stream_id, uint32_t window_size_increment, void *userdata);
102+
struct aws_h2err s_decoder_on_goaway_begin(
103+
uint32_t last_stream,
104+
uint32_t error_code,
105+
uint32_t debug_data_length,
106+
void *userdata);
101107

102108
static struct aws_http_connection_vtable s_h2_connection_vtable = {
103109
.channel_handler_vtable =
@@ -133,6 +139,7 @@ static const struct aws_h2_decoder_vtable s_h2_decoder_vtable = {
133139
.on_settings = s_decoder_on_settings,
134140
.on_settings_ack = s_decoder_on_settings_ack,
135141
.on_window_update = s_decoder_on_window_update,
142+
.on_goaway_begin = s_decoder_on_goaway_begin,
136143
};
137144

138145
static void s_lock_synced_data(struct aws_h2_connection *connection) {
@@ -279,6 +286,8 @@ static struct aws_h2_connection *s_connection_new(
279286
connection->thread_data.window_size_peer = aws_h2_settings_initial[AWS_H2_SETTINGS_INITIAL_WINDOW_SIZE];
280287
connection->thread_data.window_size_self = aws_h2_settings_initial[AWS_H2_SETTINGS_INITIAL_WINDOW_SIZE];
281288

289+
connection->thread_data.goaway_received_last_stream_id = AWS_H2_STREAM_ID_MAX;
290+
282291
/* Create a new decoder */
283292
struct aws_h2_decoder_params params = {
284293
.alloc = alloc,
@@ -1230,6 +1239,53 @@ static struct aws_h2err s_decoder_on_window_update(uint32_t stream_id, uint32_t
12301239
return AWS_H2ERR_SUCCESS;
12311240
}
12321241

1242+
struct aws_h2err s_decoder_on_goaway_begin(
1243+
uint32_t last_stream,
1244+
uint32_t error_code,
1245+
uint32_t debug_data_length,
1246+
void *userdata) {
1247+
(void)debug_data_length;
1248+
struct aws_h2_connection *connection = userdata;
1249+
1250+
if (last_stream > connection->thread_data.goaway_received_last_stream_id) {
1251+
CONNECTION_LOGF(
1252+
ERROR,
1253+
connection,
1254+
"Received GOAWAY with invalid last-stream-id=%" PRIu32 ", must not exceed previous last-stream-id=%" PRIu32,
1255+
last_stream,
1256+
connection->thread_data.goaway_received_last_stream_id);
1257+
return aws_h2err_from_h2_code(AWS_H2_ERR_PROTOCOL_ERROR);
1258+
}
1259+
/* stop sending any new stream and making new request */
1260+
aws_atomic_store_int(&connection->synced_data.new_stream_error_code, AWS_ERROR_HTTP_GOAWAY_RECEIVED);
1261+
connection->thread_data.goaway_received_last_stream_id = last_stream;
1262+
CONNECTION_LOGF(
1263+
DEBUG,
1264+
connection,
1265+
"Received GOAWAY error-code=%s(0x%x) last-stream-id=%" PRIu32,
1266+
aws_h2_error_code_to_str(error_code),
1267+
error_code,
1268+
last_stream);
1269+
/* Complete activated streams whose id is higher than last_stream, since they will not process by peer. We should
1270+
* treat them as they had never been created at all.
1271+
* This would be more efficient if we could iterate streams in reverse-id order */
1272+
struct aws_hash_iter stream_iter = aws_hash_iter_begin(&connection->thread_data.active_streams_map);
1273+
while (!aws_hash_iter_done(&stream_iter)) {
1274+
struct aws_h2_stream *stream = stream_iter.element.value;
1275+
aws_hash_iter_next(&stream_iter);
1276+
if (stream->base.id > last_stream) {
1277+
AWS_H2_STREAM_LOG(
1278+
DEBUG,
1279+
stream,
1280+
"stream ID is higher than GOAWAY last stream ID, please retry this stream on a new connection.");
1281+
s_stream_complete(connection, stream, AWS_ERROR_HTTP_GOAWAY_RECEIVED);
1282+
}
1283+
}
1284+
1285+
/* #TODO inform our user about the error_code and debug data by fire some kind of API */
1286+
return AWS_H2ERR_SUCCESS;
1287+
}
1288+
12331289
/* End decoder callbacks */
12341290

12351291
static int s_send_connection_preface_client_string(struct aws_h2_connection *connection) {
@@ -1516,6 +1572,18 @@ int aws_h2_connection_send_rst_and_close_reserved_stream(
15161572
static void s_activate_stream(struct aws_h2_connection *connection, struct aws_h2_stream *stream) {
15171573
AWS_PRECONDITION(aws_channel_thread_is_callers_thread(connection->base.channel_slot->channel));
15181574

1575+
int new_stream_error_code = (int)aws_atomic_load_int(&connection->synced_data.new_stream_error_code);
1576+
if (new_stream_error_code) {
1577+
aws_raise_error(new_stream_error_code);
1578+
AWS_H2_STREAM_LOGF(
1579+
ERROR,
1580+
stream,
1581+
"Failed activating stream, error %d (%s)",
1582+
aws_last_error(),
1583+
aws_error_name(aws_last_error()));
1584+
goto error;
1585+
}
1586+
15191587
uint32_t max_concurrent_streams = connection->thread_data.settings_peer[AWS_H2_SETTINGS_MAX_CONCURRENT_STREAMS];
15201588
if (aws_hash_table_get_entry_count(&connection->thread_data.active_streams_map) >= max_concurrent_streams) {
15211589
AWS_H2_STREAM_LOG(ERROR, stream, "Failed activating stream, max concurrent streams are reached");

source/http.c

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -116,6 +116,9 @@ static struct aws_error_info s_errors[] = {
116116
AWS_DEFINE_ERROR_INFO_HTTP(
117117
AWS_ERROR_HTTP_STREAM_IDS_EXHAUSTED,
118118
"Connection exhausted all possible stream IDs. Establish a new connection for new streams."),
119+
AWS_DEFINE_ERROR_INFO_HTTP(
120+
AWS_ERROR_HTTP_GOAWAY_RECEIVED,
121+
"Peer sent GOAWAY to initiate connection shutdown. Establish a new connection to retry the streams."),
119122
AWS_DEFINE_ERROR_INFO_HTTP(
120123
AWS_ERROR_HTTP_RST_STREAM_RECEIVED,
121124
"Peer sent RST_STREAM to terminate stream"),

tests/CMakeLists.txt

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -365,6 +365,8 @@ add_test_case(h2_client_stream_err_initial_window_size_cause_window_exceed_max)
365365
add_test_case(h2_client_stream_err_receive_rst_stream)
366366
add_test_case(h2_client_stream_receive_rst_stream_after_complete_response_ok)
367367
add_test_case(h2_client_push_promise_automatically_rejected)
368+
add_test_case(h2_client_conn_receive_goaway)
369+
add_test_case(h2_client_conn_err_invalid_last_stream_id_goaway)
368370

369371

370372
add_test_case(server_new_destroy)

tests/test_h2_client.c

Lines changed: 122 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1978,3 +1978,125 @@ TEST_CASE(h2_client_push_promise_automatically_rejected) {
19781978
client_stream_tester_clean_up(&stream_tester);
19791979
return s_tester_clean_up();
19801980
}
1981+
1982+
/* Test client receives the GOAWAY frame, stop creating new stream and complete the streams whose id are higher than the
1983+
* last stream id included in GOAWAY frame */
1984+
TEST_CASE(h2_client_conn_receive_goaway) {
1985+
ASSERT_SUCCESS(s_tester_init(allocator, ctx));
1986+
1987+
/* get connection preface and acks out of the way */
1988+
ASSERT_SUCCESS(h2_fake_peer_send_connection_preface_default_settings(&s_tester.peer));
1989+
testing_channel_drain_queued_tasks(&s_tester.testing_channel);
1990+
ASSERT_SUCCESS(h2_fake_peer_decode_messages_from_testing_channel(&s_tester.peer));
1991+
1992+
/* send multiple requests */
1993+
enum { NUM_STREAMS = 3 };
1994+
struct aws_http_message *requests[NUM_STREAMS];
1995+
struct aws_http_header request_headers_src[NUM_STREAMS][3] = {
1996+
{
1997+
DEFINE_HEADER(":method", "GET"),
1998+
DEFINE_HEADER(":scheme", "https"),
1999+
DEFINE_HEADER(":path", "/a.txt"),
2000+
},
2001+
{
2002+
DEFINE_HEADER(":method", "GET"),
2003+
DEFINE_HEADER(":scheme", "https"),
2004+
DEFINE_HEADER(":path", "/b.txt"),
2005+
},
2006+
{
2007+
DEFINE_HEADER(":method", "GET"),
2008+
DEFINE_HEADER(":scheme", "https"),
2009+
DEFINE_HEADER(":path", "/c.txt"),
2010+
},
2011+
};
2012+
struct client_stream_tester stream_testers[NUM_STREAMS];
2013+
for (size_t i = 0; i < NUM_STREAMS; ++i) {
2014+
requests[i] = aws_http_message_new_request(allocator);
2015+
aws_http_message_add_header_array(requests[i], request_headers_src[i], AWS_ARRAY_SIZE(request_headers_src[i]));
2016+
}
2017+
/* Send the first two requests */
2018+
ASSERT_SUCCESS(s_stream_tester_init(&stream_testers[0], requests[0]));
2019+
ASSERT_SUCCESS(s_stream_tester_init(&stream_testers[1], requests[1]));
2020+
testing_channel_drain_queued_tasks(&s_tester.testing_channel);
2021+
2022+
/* fake peer send a GOAWAY frame indicating only the first request will be processed */
2023+
uint32_t stream_id = aws_http_stream_get_id(stream_testers[0].stream);
2024+
struct aws_byte_cursor debug_info;
2025+
AWS_ZERO_STRUCT(debug_info);
2026+
struct aws_h2_frame *peer_frame = aws_h2_frame_new_goaway(allocator, stream_id, AWS_H2_ERR_NO_ERROR, debug_info);
2027+
ASSERT_SUCCESS(h2_fake_peer_send_frame(&s_tester.peer, peer_frame));
2028+
testing_channel_drain_queued_tasks(&s_tester.testing_channel);
2029+
2030+
/* validate the connection is still open, and the second request finished with GOAWAY_RECEIVED */
2031+
ASSERT_TRUE(aws_http_connection_is_open(s_tester.connection));
2032+
ASSERT_FALSE(stream_testers[0].complete);
2033+
ASSERT_TRUE(stream_testers[1].complete);
2034+
ASSERT_INT_EQUALS(AWS_ERROR_HTTP_GOAWAY_RECEIVED, stream_testers[1].on_complete_error_code);
2035+
2036+
/* validate the new requst will no be accepted */
2037+
ASSERT_FAILS(s_stream_tester_init(&stream_testers[2], requests[2]));
2038+
2039+
/* Try gracefully shutting down the connection */
2040+
struct aws_http_header response_headers_src[] = {DEFINE_HEADER(":status", "200")};
2041+
struct aws_http_headers *response_headers = aws_http_headers_new(allocator);
2042+
aws_http_headers_add_array(response_headers, response_headers_src, AWS_ARRAY_SIZE(response_headers_src));
2043+
struct aws_h2_frame *response_frame = aws_h2_frame_new_headers(
2044+
allocator, aws_http_stream_get_id(stream_testers[0].stream), response_headers, true /* end_stream */, 0, NULL);
2045+
ASSERT_SUCCESS(h2_fake_peer_send_frame(&s_tester.peer, response_frame));
2046+
/* shutdown channel */
2047+
aws_channel_shutdown(s_tester.testing_channel.channel, AWS_ERROR_SUCCESS);
2048+
testing_channel_drain_queued_tasks(&s_tester.testing_channel);
2049+
ASSERT_TRUE(testing_channel_is_shutdown_completed(&s_tester.testing_channel));
2050+
2051+
/* validate the first request finishes successfully */
2052+
testing_channel_drain_queued_tasks(&s_tester.testing_channel);
2053+
ASSERT_TRUE(stream_testers[0].complete);
2054+
ASSERT_INT_EQUALS(200, stream_testers[0].response_status);
2055+
2056+
ASSERT_FALSE(aws_http_connection_is_open(s_tester.connection));
2057+
2058+
/* clean up */
2059+
aws_http_headers_release(response_headers);
2060+
for (size_t i = 0; i < NUM_STREAMS; ++i) {
2061+
client_stream_tester_clean_up(&stream_testers[i]);
2062+
aws_http_message_release(requests[i]);
2063+
}
2064+
return s_tester_clean_up();
2065+
}
2066+
2067+
/* Test client receives the GOAWAY frame, stop creating new stream and complete the streams whose id are higher than the
2068+
* last stream id included in GOAWAY frame */
2069+
TEST_CASE(h2_client_conn_err_invalid_last_stream_id_goaway) {
2070+
ASSERT_SUCCESS(s_tester_init(allocator, ctx));
2071+
2072+
/* get connection preface and acks out of the way */
2073+
ASSERT_SUCCESS(h2_fake_peer_send_connection_preface_default_settings(&s_tester.peer));
2074+
testing_channel_drain_queued_tasks(&s_tester.testing_channel);
2075+
ASSERT_SUCCESS(h2_fake_peer_decode_messages_from_testing_channel(&s_tester.peer));
2076+
2077+
/* fake peer send multiple GOAWAY frames */
2078+
struct aws_byte_cursor debug_info;
2079+
AWS_ZERO_STRUCT(debug_info);
2080+
/* First on with last_stream_id as AWS_H2_STREAM_ID_MAX */
2081+
struct aws_h2_frame *peer_frame =
2082+
aws_h2_frame_new_goaway(allocator, AWS_H2_STREAM_ID_MAX, AWS_H2_ERR_NO_ERROR, debug_info);
2083+
ASSERT_SUCCESS(h2_fake_peer_send_frame(&s_tester.peer, peer_frame));
2084+
/* Second one with last_stream_id as 1 and some error */
2085+
peer_frame = aws_h2_frame_new_goaway(allocator, 1, AWS_H2_ERR_FLOW_CONTROL_ERROR, debug_info);
2086+
ASSERT_SUCCESS(h2_fake_peer_send_frame(&s_tester.peer, peer_frame));
2087+
testing_channel_drain_queued_tasks(&s_tester.testing_channel);
2088+
2089+
/* validate the connection is still open, everything is fine */
2090+
ASSERT_TRUE(aws_http_connection_is_open(s_tester.connection));
2091+
2092+
/* Another GOAWAY with higher last stream id will cause connection closed with an error */
2093+
peer_frame = aws_h2_frame_new_goaway(allocator, 3, AWS_H2_ERR_FLOW_CONTROL_ERROR, debug_info);
2094+
ASSERT_SUCCESS(h2_fake_peer_send_frame(&s_tester.peer, peer_frame));
2095+
testing_channel_drain_queued_tasks(&s_tester.testing_channel);
2096+
2097+
ASSERT_FALSE(aws_http_connection_is_open(s_tester.connection));
2098+
ASSERT_INT_EQUALS(
2099+
AWS_ERROR_HTTP_PROTOCOL_ERROR, testing_channel_get_shutdown_error_code(&s_tester.testing_channel));
2100+
/* clean up */
2101+
return s_tester_clean_up();
2102+
}

0 commit comments

Comments
 (0)