Transports
A client is anything with execute(query, variables:) whose result
to_hs into {"data" => ..., "errors" => ...} — from a full
GraphWeaver::Client down to a schema class (in-process execution), a
FakeClient, or anything you write. Every slot that takes a
client accepts any of them.
A transport is the network end of that contract — GraphQL-over-HTTP. The bundled
two — Transport::HTTP (net/http, zero dependencies, loaded by default)
and Transport::Faraday (opt-in) — subclass GraphWeaver::Transport,
which owns the shared flow: encode the request, reclassify network
failures as TransportError, raise ServerError on non-2xx, parse the
body. A subclass only implements post(body) => [status, body] — that's
the whole recipe for bringing your own HTTP client.
One-shot setup: a client
Most apps need one line:
github = GraphWeaver.new("https://api.example.com/graphql", auth: ENV["API_TOKEN"])
GraphWeaver.new builds a Client: the best transport
with auth applied (exposed as client.transport), the schema
introspected lazily, and parse/execute bound to both.
auth:— a token; "Bearer" is assumed unless the string carries its own scheme ("Basic dXNlcjpwYXNz...")headers:— anything else (API keys, custom headers)retries:— off by default;truefor aRetrywith defaults, or a Hash of its optionscache:/ttl:— schema introspection caching (see real world); url clients only — a schema source never introspects, so passing them raises- a block customizes the Faraday connection (Faraday only — raises without it)
To wire generated modules that don't bake a client, make it the app's
default: GraphWeaver.client = github. Anything satisfying the execute
contract works there — testing's auto_fake swaps in a fake per example.
Transport pick: Transport::Faraday when the app already loads
faraday (its middleware/proxy/timeout ecosystem comes along), the
zero-dependency Transport::HTTP otherwise. Detection is defined?(Faraday) —
deliberately not a require: faraday rides along transitively in most
bundles (stripe, octokit, ...), and try-requiring would silently switch
transports on apps that never chose it. With faraday under
require: false, load it before building the client.
Building blocks
The client is convenience, not the only door — construct and assign yourself for full control:
# zero-dependency Net::HTTP — persistent (keep-alive) connection,
# mutex-serialized; timeouts raise retriable TransportError
GraphWeaver::Transport::HTTP.new(
url,
headers: { ... },
open_timeout: 10, read_timeout: 30, # seconds (the defaults)
keep_alive_timeout: 2, # idle window before reconnecting
)
# Faraday: a url (+ optional middleware block), or a ready connection
GraphWeaver::Transport::Faraday.new(url) do |conn|
conn.request :authorization, "Bearer", -> { Tokens.fetch } # dynamic tokens
conn.response :logger
end
GraphWeaver::Transport::Faraday.new(MyApp.faraday_connection)
# One Faraday::Connection is reused for the transport's lifetime, but
# socket keep-alive depends on the ADAPTER: Faraday's default net_http
# adapter opens a fresh connection per request. For persistent sockets
# (and real pooling), pick a persistent adapter:
GraphWeaver::Transport::Faraday.new(url) do |conn|
conn.adapter :net_http_persistent # gem "net-http-persistent"
end
GraphWeaver.client = ... # the app default (a Client or any of the above)
Client resolution
The canonical order — how a generated module finds its client (each slot
takes a Client or any bare transport/fake):
- per call:
execute(some_client, ...)— the optional first positional argument, so variables keep the entire kwarg namespace - per module:
MyQuery.client = something - baked constant:
Codegen.generate(..., client: MyApi::CLIENT) - the app default:
GraphWeaver.client=
Nothing set anywhere raises with a message saying which knobs exist.
Retries
Retry wraps any client/transport:
GraphWeaver::Retry.new(
inner_transport,
tries: 5, # total attempts, first included
backoff: :exponential, # or :linear, or ->(attempt) { seconds }
base: 0.5, max: 30, # seconds; delays clamp at max:
jitter: true, # randomize each delay by 50-100%
on: [GraphWeaver::TransportError, GraphWeaver::ServerError],
retry_if: ->(error) { ... }, # fine-grain within on:
retry_codes: ["THROTTLED"], # also retry GraphQL errors by code
)
Defaults: transport failures always retry (the request never arrived);
ServerError only on 5xx — a 4xx is a bug in the request, retrying won't
fix it. retry_codes: re-inspects response envelopes so GraphQL-level
throttling can retry too (off by default — pass the codes your API uses).
Exhausting tries: re-raises the last error (or returns the last
code-matched response).
Or via the client: GraphWeaver.new(url, retries: { tries: 5, retry_codes: ["THROTTLED"] }).
What classifies as a transport failure is an extensible set — see
errors (GraphWeaver.register_transport_error).