Skip to content

Commit 0b2a7ca

Browse files
authored
Rewind request bodies before retry. (#229)
1 parent 62af2d7 commit 0b2a7ca

4 files changed

Lines changed: 70 additions & 3 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.6"
31-
spec.add_dependency "protocol-http", "~> 0.62"
31+
spec.add_dependency "protocol-http", "~> 0.64"
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 & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -121,7 +121,7 @@ def call(request)
121121
connection = nil
122122
end
123123

124-
if attempt < @retries
124+
if attempt < @retries and request.rewind!
125125
retry
126126
else
127127
raise
@@ -132,7 +132,7 @@ def call(request)
132132
connection = nil
133133
end
134134

135-
if request.idempotent? and attempt < @retries
135+
if attempt < @retries and request.retry!
136136
retry
137137
else
138138
raise

‎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+
- Rewind request bodies before retrying requests.
6+
37
## v0.97.0
48

59
- Exposed all supported protocol names from the plaintext HTTP protocol negotiator.

‎test/async/http/client/retry.rb‎

Lines changed: 63 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,63 @@
1+
# frozen_string_literal: true
2+
3+
# Released under the MIT License.
4+
# Copyright, 2026, by Samuel Williams.
5+
6+
require "async/http/client"
7+
8+
class RetryClient < Async::HTTP::Client
9+
def initialize(failures)
10+
@pool = FakePool.new
11+
@protocol = nil
12+
@retries = 3
13+
@scheme = "https"
14+
@authority = "example.com"
15+
@failures = failures
16+
@chunks = []
17+
end
18+
19+
attr :chunks
20+
21+
class FakePool
22+
def acquire
23+
Object.new
24+
end
25+
26+
def release(connection)
27+
end
28+
end
29+
30+
def make_response(request, connection, attempt)
31+
@chunks << request.body&.read
32+
33+
if failure = @failures.shift
34+
raise failure
35+
end
36+
37+
return Protocol::HTTP::Response[200, {}, ["OK"]]
38+
end
39+
end
40+
41+
describe Async::HTTP::Client do
42+
with "retries" do
43+
it "rewinds idempotent request bodies after ambiguous failures" do
44+
client = RetryClient.new([EOFError.new])
45+
request = Protocol::HTTP::Request["PUT", "/", {}, ["Hello"]]
46+
47+
response = client.call(request)
48+
49+
expect(response).to be(:success?)
50+
expect(client.chunks).to be == ["Hello", "Hello"]
51+
end
52+
53+
it "rewinds non-idempotent request bodies after refused requests" do
54+
client = RetryClient.new([Protocol::HTTP::RefusedError.new("Request not processed.")])
55+
request = Protocol::HTTP::Request["POST", "/", {}, ["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+
end
63+
end

0 commit comments

Comments
 (0)