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)