NAME
Open::API::Client - a spec driven HTTP client using Fetch
SYNOPSIS
my $client = Open::API::Client->new(
spec => 'openapi.json', # or api => $open_api
base_url => 'http://127.0.0.1:3000',
);
my $res = $client->call('getPet', petId => 42)->get;
# { status => 200, headers => {...}, data => { id => 42, ... } }
# operationId sugar (same as call):
$res = $client->getPet(petId => 42)->get;
DESCRIPTION
Builds requests from the same compiled OpenAPI 3.1 document the server side uses: parameters are validated client-side through the compiled JSON::Schema::Fast handles BEFORE any I/O (bad input croaks), the URL, query, headers, cookies and JSON body are assembled in C, and the request is fired through Fetch's C ABI. Every call returns a Fetch::Future: get awaits it synchronously, or pass loop => at construction to share an event loop and run calls concurrently.
The future resolves to a hashref: status, headers (lowercased names), data (JSON-decoded body when the response is application/json, raw bytes otherwise), and on failure error (with status 0 for transport errors). With validate => 1, a response that does not match the operation's response schema gets error and errors set.
CONSTRUCTOR
Open::API::Client->new(%opts);
api (an Open::API) or spec (anything "new" in Open::API accepts) is required, as is base_url. validate (default 0) checks response bodies against the spec. All other options are passed to Fetch's constructor on first use, so everything the UA supports works here:
my $client = Open::API::Client->new(
spec => 'openapi.json',
base_url => 'https://api.example.com',
cookie_jar => 1, # capture + replay cookies
headers => { Authorization => "Bearer $t" }, # on every request
agent => 'MyApp/1.0',
timeout => 10,
tls_verify => 1,
pool_size => 64,
loop => $shared_loop, # async on one loop
);
The cookie jar captures Set-Cookie from responses and replays cookies on later calls automatically; default headers are merged under any parameters the operation declares. Two sharp edges: per-call values are matched to declared parameter names only (undeclared %params keys are ignored - put undeclared headers such as Authorization in the UA headers), and when an operation declares cookie parameters the built Cookie header takes the place of the jar's cookies for that call.
The security option (see "SECURITY") is spec-driven authentication: give it the credentials once and each call attaches whatever the operation's security requirements ask for.
The csrf option (see "CSRF") makes a CSRF-protected server work with no per-call code.
CSRF
If the server enforces CSRF (see "CSRF" in Open::API), csrf => 1 makes the client satisfy it transparently:
my $client = Open::API::Client->new(
spec => 'openapi.json',
base_url => 'https://api.example.com',
cookie_jar => 1, # carry the session cookie
csrf => 1, # handle the CSRF handshake
);
$client->listPets->get; # a safe call seeds the token
$client->createPet(body => \%pet)->get; # POST just works
On a state-changing method (POST/PUT/PATCH/DELETE) the client presents the Origin the server checks and echoes the current CSRF token in the token header; from each response it captures the token the server sets, so it follows single-use rotation on its own. Do a safe call first (the usual "load the page" request) so the server can hand out the initial token. cookie_jar carries the session cookie the token is bound to.
csrf => 1 uses the defaults { cookie => 'csrf', header => 'X-CSRF-Token', origin => <derived from base_url> }; pass a hashref to override any of them:
csrf => { header => 'X-XSRF-Token', cookie => 'xsrf',
origin => 'https://app.example.com' },
SECURITY
Pass a security map of scheme name (from the spec's components.securitySchemes) to a credential, and every call attaches what its operation requires - the mirror image of the server's checker map:
my $client = Open::API::Client->new(
spec => 'openapi.json',
base_url => 'https://api.example.com',
security => {
ApiKey => $api_key, # apiKey: header/query/cookie
BearerAuth => $access_token, # http bearer / oauth2 / oidc
BasicAuth => [ $user, $password ], # http basic (or "user:pass")
},
);
$client->getPet(petId => 42)->get; # credentials attached automatically
How each credential is sent follows its scheme: an apiKey goes in the named header, query parameter or cookie; a bearer (or oauth2 / openIdConnect) token becomes Authorization: Bearer $token; a basic credential - a "user:pass" string or a [ $user, $pass ] pair - is base64-encoded into Authorization: Basic ....
When an operation lists several alternatives (an OR), the client uses the first one whose schemes are all present in the security map; an AND alternative attaches every scheme in it. If no alternative can be satisfied from the credentials you supplied, the call croaks before any I/O rather than firing an unauthenticated request. Operations with no security requirement send nothing extra.
One scheme type is exempt from that croak: an apiKey in a cookie (a session cookie set at login, say) is ambient - the cookie jar carries it - so the client does not require an explicit credential for it and does not attach one unless you pass it. Enable cookie_jar and log in first; the session cookie then satisfies the scheme on its own. Every other scheme (header/query apiKey, bearer, basic) is not ambient and must be in the security map.
METHODS
call
my $future = $client->call($operationId, %params);
Flat %params are matched to the operation's declared parameters by name (body is the request body). Croaks before any I/O when a required parameter is missing or a value fails its schema.
api
The underlying Open::API object.
AUTHOR
LNATION <email@lnation.org>
LICENSE AND COPYRIGHT
This software is Copyright (c) 2026 by LNATION <email@lnation.org>.
This is free software, licensed under:
The Artistic License 2.0 (GPL Compatible)