Skip to content

Commit 1550096

Browse files
Retry remote HTTP/2 internal errors
1 parent 555ebe5 commit 1550096

6 files changed

Lines changed: 54 additions & 2 deletions

File tree

async-http.gemspec

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -28,7 +28,7 @@ Gem::Specification.new do |spec|
2828
spec.add_dependency "async-pool", "~> 0.11"
2929
spec.add_dependency "io-endpoint", "~> 0.14"
3030
spec.add_dependency "io-stream", "~> 0.14"
31-
spec.add_dependency "protocol-http", "~> 0.64"
31+
spec.add_dependency "protocol-http", "~> 0.66"
3232
spec.add_dependency "protocol-http1", "~> 0.39"
3333
spec.add_dependency "protocol-http2", "~> 0.26"
3434
spec.add_dependency "protocol-url", "~> 0.2"

lib/async/http/client.rb

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@
99
require "async/pool/controller"
1010

1111
require "protocol/http/body/completable"
12+
require "protocol/http/error"
1213
require "protocol/http/methods"
1314

1415
require_relative "protocol"
@@ -126,7 +127,7 @@ def call(request)
126127
else
127128
raise
128129
end
129-
rescue SocketError, IOError, EOFError, Errno::ECONNRESET, Errno::EPIPE
130+
rescue ::Protocol::HTTP::RemoteError, SocketError, IOError, EOFError, Errno::ECONNRESET, Errno::EPIPE
130131
if connection
131132
@pool.release(connection)
132133
connection = nil

lib/async/http/protocol/http2/client.rb

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -54,6 +54,12 @@ def write_request(response, request)
5454
# @parameter response [Response] The response to wait on.
5555
def read_response(response)
5656
response.wait
57+
rescue ::Protocol::HTTP2::StreamError => error
58+
if error.code == ::Protocol::HTTP2::Error::INTERNAL_ERROR
59+
raise ::Protocol::HTTP::RemoteError.new(error.message), cause: error
60+
end
61+
62+
raise
5763
end
5864
end
5965
end

releases.md

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,9 @@
11
# Releases
22

3+
## Unreleased
4+
5+
- Retry safe requests when a remote HTTP/2 endpoint resets the stream with `INTERNAL_ERROR` before returning a response.
6+
37
## v0.98.1
48

59
- Probe idle HTTP/1 connections before reuse, avoiding requests on connections already closed by the peer.

test/async/http/client/retry.rb

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -50,6 +50,27 @@ def make_response(request, connection, attempt)
5050
expect(client.chunks).to be == ["Hello", "Hello"]
5151
end
5252

53+
it "rewinds idempotent request bodies after remote failures" do
54+
client = RetryClient.new([Protocol::HTTP::RemoteError.new("Remote endpoint failed.")])
55+
request = Protocol::HTTP::Request["PUT", "/", {}, ["Hello"]]
56+
57+
response = client.call(request)
58+
59+
expect(response).to be(:success?)
60+
expect(client.chunks).to be == ["Hello", "Hello"]
61+
end
62+
63+
it "does not retry non-idempotent requests after remote failures" do
64+
client = RetryClient.new([Protocol::HTTP::RemoteError.new("Remote endpoint failed.")])
65+
request = Protocol::HTTP::Request["POST", "/", {}, ["Hello"]]
66+
67+
expect do
68+
client.call(request)
69+
end.to raise_exception(Protocol::HTTP::RemoteError)
70+
71+
expect(client.chunks).to be == ["Hello"]
72+
end
73+
5374
it "rewinds non-idempotent request bodies after refused requests" do
5475
client = RetryClient.new([Protocol::HTTP::RefusedError.new("Request not processed.")])
5576
request = Protocol::HTTP::Request["POST", "/", {}, ["Hello"]]

test/async/http/protocol/http2/connection_close_with_active_streams.rb

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -49,6 +49,26 @@
4949
end.wait
5050
end
5151

52+
it "maps a remote internal error while waiting for headers" do
53+
Async do |task|
54+
client_connection = Async::HTTP::Protocol::HTTP2::Client.new(client_stream)
55+
client_connection.open!
56+
57+
response = client_connection.create_response
58+
response.stream.close!(Protocol::HTTP2::INTERNAL_ERROR)
59+
60+
expect do
61+
client_connection.read_response(response)
62+
end.to raise_exception(Protocol::HTTP::RemoteError).and(
63+
have_attributes(
64+
cause: be_a(Protocol::HTTP2::StreamError).and(
65+
have_attributes(code: be == Protocol::HTTP2::INTERNAL_ERROR)
66+
)
67+
)
68+
)
69+
end.wait
70+
end
71+
5272
it "does not raise error when connection closes without active streams" do
5373
Async do |task|
5474
# Create client connection

0 commit comments

Comments
 (0)