Skip to content

Commit 82c7137

Browse files
Add guide for choosing a client. (#242)
1 parent f2ac865 commit 82c7137

3 files changed

Lines changed: 168 additions & 2 deletions

File tree

‎guides/choosing-a-client/readme.md‎

Lines changed: 164 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,164 @@
1+
# Choosing a Client
2+
3+
This guide explains how to choose between ruby:`Async::HTTP::Internet`, ruby:`Async::HTTP::Client`, and higher-level interfaces for libraries.
4+
5+
ruby:`Async::HTTP::Internet` and ruby:`Async::HTTP::Client` use the same request and response model. The important differences are how destinations are selected, where connection settings are applied, and who owns the client life cycle.
6+
7+
## Quick Decision
8+
9+
| Situation | Interface | Why |
10+
| --- | --- | --- |
11+
| Requests may target different origins and the defaults are suitable. | Shared ruby:`Async::HTTP::Internet` | Selects and reuses a client for each origin automatically. |
12+
| Requests may target different origins, but need common client options or explicit ownership. | Explicit ruby:`Async::HTTP::Internet` | Applies the same options to each managed client and can be injected or closed early. |
13+
| Requests repeatedly target one configured origin. | ruby:`Async::HTTP::Client` | Exposes the endpoint, protocol, retry, and connection-pool configuration directly. |
14+
| A library wraps one HTTP service directly. | Injected ruby:`Async::HTTP::Client` or `Protocol::HTTP` middleware | Leaves transport configuration, ownership, and testing under application control. |
15+
| A library models an HTTP API as resources and representations. | [`async-rest`](https://socketry.github.io/async-rest/guides/getting-started/) | Provides higher-level API modeling over an injectable `Protocol::HTTP` delegate. |
16+
| A library uses Faraday as its HTTP abstraction. | [`async-http-faraday`](https://socketry.github.io/async-http-faraday/guides/getting-started/) | Lets the application retain the Faraday interface while using `Async::HTTP` as the transport. |
17+
18+
Application code can start with the shared `Internet` interface unless it has a specific ownership or configuration requirement. Library code should accept an explicit HTTP dependency.
19+
20+
## Shared Internet for General Requests
21+
22+
The shared `Internet` interface is the simplest choice for requests to arbitrary URLs. It maintains one client for each origin and reuses persistent connections:
23+
24+
~~~ ruby
25+
require "async/http/internet/instance"
26+
27+
urls = [
28+
"https://www.ruby-lang.org/en/",
29+
"https://example.com/",
30+
]
31+
32+
Sync do
33+
urls.each do |url|
34+
Async::HTTP::Internet.get(url) do |response|
35+
puts "#{url}: #{response.status}"
36+
end
37+
end
38+
end
39+
~~~
40+
41+
The class-level interface uses a thread-local `Internet` instance. Its connection pools are bound to the event loop and close when that event loop exits. The response block closes each response after it is processed.
42+
43+
Use the shared interface when:
44+
45+
- The application requests URLs from multiple or dynamically selected origins.
46+
- Default retry and connection-pool settings are suitable.
47+
- The client does not need to be injected as an application dependency.
48+
49+
## Explicit Internet for Shared Configuration
50+
51+
An explicit `Internet` provides the same per-origin client selection while making ownership and client options visible. Options are passed to every client it creates; for example, `limit` applies independently to the pool for each origin:
52+
53+
~~~ ruby
54+
require "async/http/internet"
55+
56+
Sync do
57+
internet = Async::HTTP::Internet.new(retries: 1, limit: 4)
58+
59+
begin
60+
internet.get("https://www.ruby-lang.org/en/") do |response|
61+
puts response.status
62+
end
63+
ensure
64+
internet.close
65+
end
66+
end
67+
~~~
68+
69+
Use an explicit `Internet` when:
70+
71+
- Several origins should share the same retry or pool settings.
72+
- The client should be injected into another object or replaced during testing.
73+
- Connections should be released before the event loop exits.
74+
75+
## Client for One Endpoint
76+
77+
A `Client` targets one ruby:`Async::HTTP::Endpoint`. Use it when a remote service is a stable part of the application architecture and needs its own protocol, TLS, retry, or pool configuration:
78+
79+
~~~ ruby
80+
require "async/http"
81+
82+
endpoint = Async::HTTP::Endpoint.parse("https://httpbin.org")
83+
84+
Sync do
85+
Async::HTTP::Client.open(endpoint, retries: 1, limit: 4) do |client|
86+
response = client.get("/status/200")
87+
88+
begin
89+
puts response.status
90+
ensure
91+
response.close
92+
end
93+
end
94+
end
95+
~~~
96+
97+
Client convenience methods accept a path rather than a complete URL. They return a response that the caller must close. `Client.open` closes the client and its connection pool when the block exits.
98+
99+
Reuse a client for repeated requests rather than creating one per request; otherwise the application cannot benefit from persistent connections.
100+
101+
## Building a Library That Makes HTTP Requests
102+
103+
A library should generally accept its HTTP client as an explicit dependency. This lets the application configure connection limits, retries, proxies, instrumentation, and test doubles without the library creating hidden global state:
104+
105+
~~~ ruby
106+
require "async/http"
107+
108+
class StatusService
109+
def initialize(client)
110+
@client = client
111+
end
112+
113+
def healthy?
114+
response = @client.get("/status/200")
115+
response.status == 200
116+
ensure
117+
response&.close
118+
end
119+
end
120+
121+
endpoint = Async::HTTP::Endpoint.parse("https://httpbin.org")
122+
123+
Sync do
124+
Async::HTTP::Client.open(endpoint) do |client|
125+
puts StatusService.new(client).healthy?
126+
end
127+
end
128+
~~~
129+
130+
The library does not close an injected client because the caller owns it and may share it with other components. If the library also provides an `open` convenience method that constructs a client, that method should close the client it creates when its block exits.
131+
132+
Define the accepted interface precisely. A ruby:`Async::HTTP::Client` is bound to one endpoint and its convenience methods accept relative paths, while ruby:`Async::HTTP::Internet` selects an endpoint from a complete URL. They should not be treated as interchangeable merely because both provide methods such as `get`. If the library constructs ruby:`Protocol::HTTP::Request` objects and only calls `call`, it can accept a `Protocol::HTTP` middleware delegate instead of requiring a concrete client.
133+
134+
## Modeling Resources with async-rest
135+
136+
Use [`async-rest`](https://socketry.github.io/async-rest/guides/getting-started/) when a library benefits from modeling a remote HTTP API as resources and representations rather than exposing request operations directly.
137+
138+
ruby:`Async::REST::Resource` accepts a `Protocol::HTTP` middleware delegate, so the application can supply and configure the transport. Its `open` method provides the complementary convenience interface: it creates a ruby:`Async::HTTP::Client`, yields the resource, and closes the client when the block exits.
139+
140+
## Supporting Faraday with async-http-faraday
141+
142+
Use [`async-http-faraday`](https://socketry.github.io/async-http-faraday/guides/getting-started/) when a library uses Faraday as its public HTTP abstraction or needs compatibility with the Faraday ecosystem. A new Async-native library can usually accept a ruby:`Async::HTTP::Client` or `Protocol::HTTP` middleware delegate directly.
143+
144+
If a library uses Faraday, accept a configured `Faraday::Connection` rather than changing `Faraday.default_adapter` globally. The application can then select the `Async::HTTP` adapter for that connection:
145+
146+
~~~ ruby
147+
require "async/http/faraday"
148+
149+
class StatusService
150+
def initialize(connection)
151+
@connection = connection
152+
end
153+
154+
def healthy?
155+
@connection.get("/status/200").success?
156+
end
157+
end
158+
159+
connection = Faraday.new("https://httpbin.org") do |builder|
160+
builder.adapter :async_http
161+
end
162+
163+
puts StatusService.new(connection).healthy?
164+
~~~

‎guides/getting-started/readme.md‎

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -20,7 +20,7 @@ $ bundle add async-http
2020
- ruby:`Async::HTTP::Endpoint` describes how a client connects or a server listens, including the URL, protocol, and TLS configuration.
2121
- [`protocol-http`](https://github.com/socketry/protocol-http) provides the shared request, response, header, and body interfaces.
2222

23-
Use `Internet` for general-purpose requests to different hosts. Use `Client` when your application repeatedly communicates with one endpoint or needs endpoint-specific configuration.
23+
Use `Internet` for general-purpose requests to different hosts. Use `Client` when your application repeatedly communicates with one endpoint or needs endpoint-specific configuration. See [Choosing a Client](../choosing-a-client/) for the ownership and configuration trade-offs.
2424

2525
## Making a Request
2626

‎guides/links.yaml‎

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,6 @@
11
getting-started:
22
order: 0
3-
testing:
3+
choosing-a-client:
44
order: 1
5+
testing:
6+
order: 2

0 commit comments

Comments
 (0)