diff --git a/async-http.gemspec b/async-http.gemspec index 41caf1d9..cb325cf5 100644 --- a/async-http.gemspec +++ b/async-http.gemspec @@ -28,7 +28,7 @@ Gem::Specification.new do |spec| spec.add_dependency "async-pool", "~> 0.11" spec.add_dependency "io-endpoint", "~> 0.14" spec.add_dependency "io-stream", "~> 0.6" - spec.add_dependency "protocol-http", "~> 0.62" + spec.add_dependency "protocol-http", "~> 0.64" spec.add_dependency "protocol-http1", "~> 0.39" spec.add_dependency "protocol-http2", "~> 0.26" spec.add_dependency "protocol-url", "~> 0.2" diff --git a/lib/async/http/client.rb b/lib/async/http/client.rb index 56bc9f6b..5c5add94 100755 --- a/lib/async/http/client.rb +++ b/lib/async/http/client.rb @@ -121,7 +121,7 @@ def call(request) connection = nil end - if attempt < @retries + if attempt < @retries and request.rewind! retry else raise @@ -132,7 +132,7 @@ def call(request) connection = nil end - if request.idempotent? and attempt < @retries + if attempt < @retries and request.retry! retry else raise diff --git a/releases.md b/releases.md index 778138a9..dcc70003 100644 --- a/releases.md +++ b/releases.md @@ -1,5 +1,9 @@ # Releases +## Unreleased + + - Rewind request bodies before retrying requests. + ## v0.97.0 - Exposed all supported protocol names from the plaintext HTTP protocol negotiator. diff --git a/test/async/http/client/retry.rb b/test/async/http/client/retry.rb new file mode 100644 index 00000000..7111541f --- /dev/null +++ b/test/async/http/client/retry.rb @@ -0,0 +1,63 @@ +# frozen_string_literal: true + +# Released under the MIT License. +# Copyright, 2026, by Samuel Williams. + +require "async/http/client" + +class RetryClient < Async::HTTP::Client + def initialize(failures) + @pool = FakePool.new + @protocol = nil + @retries = 3 + @scheme = "https" + @authority = "example.com" + @failures = failures + @chunks = [] + end + + attr :chunks + + class FakePool + def acquire + Object.new + end + + def release(connection) + end + end + + def make_response(request, connection, attempt) + @chunks << request.body&.read + + if failure = @failures.shift + raise failure + end + + return Protocol::HTTP::Response[200, {}, ["OK"]] + end +end + +describe Async::HTTP::Client do + with "retries" do + it "rewinds idempotent request bodies after ambiguous failures" do + client = RetryClient.new([EOFError.new]) + request = Protocol::HTTP::Request["PUT", "/", {}, ["Hello"]] + + response = client.call(request) + + expect(response).to be(:success?) + expect(client.chunks).to be == ["Hello", "Hello"] + end + + it "rewinds non-idempotent request bodies after refused requests" do + client = RetryClient.new([Protocol::HTTP::RefusedError.new("Request not processed.")]) + request = Protocol::HTTP::Request["POST", "/", {}, ["Hello"]] + + response = client.call(request) + + expect(response).to be(:success?) + expect(client.chunks).to be == ["Hello", "Hello"] + end + end +end