|
19 | 19 | connect/4, |
20 | 20 | close/1, |
21 | 21 | is_open/1, |
| 22 | + is_alive/1, |
22 | 23 | get_socket/1, |
23 | 24 | set_mode/2, |
24 | 25 | controlling_process/2, |
|
82 | 83 | invalid_status_line |
83 | 84 | | invalid_header |
84 | 85 | | invalid_chunk_size |
| 86 | + | chunk_size_overflow |
| 87 | + | too_many_headers |
| 88 | + | conflicting_content_length |
| 89 | + | invalid_trailer |
85 | 90 | | invalid_content_length |
86 | 91 | | {unknown_transfer_encoding, binary()} |
87 | 92 | | {parse_error, term()}. |
|
111 | 116 | connection_closed |
112 | 117 | | pipeline_full |
113 | 118 | | {invalid_request_ref, reference()} |
114 | | - | unexpected_close. |
| 119 | + | unexpected_close |
| 120 | + | {incomplete_body, non_neg_integer(), non_neg_integer()} |
| 121 | + | incomplete_chunked_body. |
115 | 122 |
|
116 | 123 | %% @doc Structured error reason. |
117 | 124 | %% |
@@ -377,6 +384,19 @@ is_open(Conn) when is_tuple(Conn) -> |
377 | 384 | gen_http_h2_conn -> gen_http_h2:is_open(Conn) |
378 | 385 | end. |
379 | 386 |
|
| 387 | +%% @doc Check if the underlying socket is still alive. |
| 388 | +%% |
| 389 | +%% Does a non-blocking recv to detect if the server has closed the connection |
| 390 | +%% while it was idle. Useful for connection pool implementations. |
| 391 | +%% |
| 392 | +%% Works with both HTTP/1.1 and HTTP/2 connections. |
| 393 | +-spec is_alive(conn()) -> boolean(). |
| 394 | +is_alive(Conn) when is_tuple(Conn) -> |
| 395 | + case element(1, Conn) of |
| 396 | + gen_http_h1_conn -> gen_http_h1:is_alive(Conn); |
| 397 | + gen_http_h2_conn -> gen_http_h2:is_alive(Conn) |
| 398 | + end. |
| 399 | + |
380 | 400 | %% @doc Get the underlying socket. |
381 | 401 | %% |
382 | 402 | %% Works with both HTTP/1.1 and HTTP/2 connections. |
@@ -474,6 +494,10 @@ delete_private(Conn, Key) when is_tuple(Conn) -> |
474 | 494 | is_retriable_error({transport_error, _}) -> |
475 | 495 | %% Transport errors are generally retriable |
476 | 496 | true; |
| 497 | +is_retriable_error({protocol_error, {rst_stream, refused_stream}}) -> |
| 498 | + %% RFC 9113 Section 8.7: server rejected the stream without processing it. |
| 499 | + %% Safe to retry on a new connection. |
| 500 | + true; |
477 | 501 | is_retriable_error({protocol_error, _}) -> |
478 | 502 | %% Protocol errors indicate incompatibility or bugs |
479 | 503 | false; |
@@ -617,11 +641,26 @@ is_not_retriable_protocol_errors_test() -> |
617 | 641 | ?assertNot(is_retriable_error({protocol_error, {frame_size_error, 100}})), |
618 | 642 | ?assertNot(is_retriable_error({protocol_error, max_concurrent_streams_exceeded})). |
619 | 643 |
|
| 644 | +refused_stream_is_retriable_test() -> |
| 645 | + %% RFC 9113 Section 8.7: refused_stream means the server rejected |
| 646 | + %% the stream without processing it, safe to retry |
| 647 | + ?assert(is_retriable_error({protocol_error, {rst_stream, refused_stream}})). |
| 648 | + |
| 649 | +other_rst_stream_not_retriable_test() -> |
| 650 | + %% Other RST_STREAM error codes are not retriable |
| 651 | + ?assertNot(is_retriable_error({protocol_error, {rst_stream, stream_closed}})), |
| 652 | + ?assertNot(is_retriable_error({protocol_error, {rst_stream, cancel}})). |
| 653 | + |
620 | 654 | is_retriable_some_application_errors_test() -> |
621 | 655 | %% Some application errors are retriable |
622 | 656 | ?assert(is_retriable_error({application_error, connection_closed})), |
623 | 657 | ?assert(is_retriable_error({application_error, unexpected_close})). |
624 | 658 |
|
| 659 | +incomplete_body_errors_not_retriable_test() -> |
| 660 | + %% Incomplete body errors are application-level, not retriable |
| 661 | + ?assertNot(is_retriable_error({application_error, {incomplete_body, 500, 1000}})), |
| 662 | + ?assertNot(is_retriable_error({application_error, incomplete_chunked_body})). |
| 663 | + |
625 | 664 | is_not_retriable_some_application_errors_test() -> |
626 | 665 | %% Some application errors are not retriable |
627 | 666 | ?assertNot(is_retriable_error({application_error, pipeline_full})), |
|
0 commit comments