From e9f60059a5692c4173e76f0da5a3127afc32ff99 Mon Sep 17 00:00:00 2001 From: Samuel Williams Date: Thu, 30 Jul 2026 13:41:18 +1200 Subject: [PATCH] Handle remote HTTP errors --- async-http-faraday.gemspec | 2 +- context/getting-started.md | 15 ++++++++++++ gems.rb | 1 + lib/async/http/faraday/adapter.rb | 1 + test/async/http/faraday/adapter.rb | 37 ++++++++++++++++++++++++++++++ 5 files changed, 55 insertions(+), 1 deletion(-) diff --git a/async-http-faraday.gemspec b/async-http-faraday.gemspec index adbfe1e..db03493 100644 --- a/async-http-faraday.gemspec +++ b/async-http-faraday.gemspec @@ -25,6 +25,6 @@ Gem::Specification.new do |spec| spec.required_ruby_version = ">= 3.3" - spec.add_dependency "async-http", "~> 0.42" + spec.add_dependency "async-http", "~> 0.99" spec.add_dependency "faraday" end diff --git a/context/getting-started.md b/context/getting-started.md index e4e195a..180846f 100644 --- a/context/getting-started.md +++ b/context/getting-started.md @@ -48,6 +48,21 @@ connection = Faraday.new(...) do |builder| end ~~~ +### Retrying Remote Failures + +When a remote endpoint fails while processing a request, the adapter raises {ruby Faraday::ConnectionFailed}. You can use the `faraday-retry` middleware to retry idempotent requests: + +~~~ruby +require "faraday/retry" + +connection = Faraday.new(...) do |builder| + builder.request :retry, exceptions: [Faraday::ConnectionFailed] + builder.adapter :async_http +end +~~~ + +By default, `faraday-retry` only retries idempotent methods. Configure its retry policy explicitly before retrying other methods. + The value of isolation cannot be overstated - if you can design you program using a share-nothing (between threads) architecture, you will have a much easier time debugging and reasoning about your program, however this comes at the cost of increased resource usage. Alternatively, if you do not want to cache client connections, you can use the `Async::HTTP::Faraday::Clients` interface, which closes the connection after each request: diff --git a/gems.rb b/gems.rb index 5bb15a3..48f675d 100644 --- a/gems.rb +++ b/gems.rb @@ -28,6 +28,7 @@ gem "bake-test-external" gem "faraday-multipart" + gem "faraday-retry" gem "sus-fixtures-async" gem "sus-fixtures-async-http" diff --git a/lib/async/http/faraday/adapter.rb b/lib/async/http/faraday/adapter.rb index 82a0ea4..b9db603 100644 --- a/lib/async/http/faraday/adapter.rb +++ b/lib/async/http/faraday/adapter.rb @@ -105,6 +105,7 @@ def self.setup_parallel_manager(**options) # The exceptions that are considered connection errors and result in a `Faraday::ConnectionFailed` exception. CONNECTION_EXCEPTIONS = [ + ::Protocol::HTTP::RemoteError, Errno::EADDRNOTAVAIL, Errno::ECONNABORTED, Errno::ECONNREFUSED, diff --git a/test/async/http/faraday/adapter.rb b/test/async/http/faraday/adapter.rb index eb5b94a..d421537 100644 --- a/test/async/http/faraday/adapter.rb +++ b/test/async/http/faraday/adapter.rb @@ -16,6 +16,7 @@ require "faraday" require "faraday/multipart" +require "faraday/retry" require "protocol/http/body/file" require "protocol/multipart" @@ -268,6 +269,42 @@ def get_response(url = bound_url, path = "/index", adapter_options: {}) end end + with "a remote failure" do + it "can retry using Faraday middleware" do + attempts = 0 + client = Object.new + client.define_singleton_method(:call) do |request| + attempts += 1 + + if attempts == 1 + body = Protocol::HTTP::Body::Writable.new + body.close_write(Protocol::HTTP::RemoteError.new("The remote endpoint failed!")) + Protocol::HTTP::Response[200, {}, body] + else + Protocol::HTTP::Response[200, {}, ["Hello World"]] + end + end + + clients = Object.new + clients.define_singleton_method(:with_client) do |endpoint, &block| + block.call(client) + end + clients.define_singleton_method(:close){} + + connection = Faraday.new("https://example.com") do |builder| + builder.request :retry, max: 1, exceptions: [Faraday::ConnectionFailed] + builder.adapter :async_http, clients: proc{clients} + end + + response = connection.get("/") + + expect(response.body).to be == "Hello World" + expect(attempts).to be == 2 + ensure + connection&.close + end + end + with "a multi-part post body" do include Sus::Fixtures::Async::HTTP::ServerContext