NAME

HTTP::API::Core - Small foundation for JSON HTTP API cores

SYNOPSIS

use HTTP::API::Core;

my $api = HTTP::API::Core->new(
    base_url => 'https://api.example.com',
    headers  => { Authorization => "Bearer $token" },
    timeout  => 10,
    retry    => {
        attempts   => 3,
        base_delay => 0.25,
        max_delay  => 5,
        jitter     => 1,
    },
    hooks => {
        before_request => sub {
            my ($ctx) = @_;
            $ctx->{headers}{'X-Trace-Id'} = make_trace_id();
        },
    },
);

my $response = $api->get('/users');
my $users = $response->json;
my $rate = $response->rate_limit;

my $pager = $api->paginate(
    '/users',
    mode  => 'cursor',
    items => 'data.users',
    next  => 'meta.next_cursor',
);

while (my $user = $pager->next) {
    ...
}

DESCRIPTION

HTTP::API::Core is a deliberately small base layer for building HTTP API clients. It provides base URL handling, default headers, JSON request/response helpers, timeout configuration, structured errors, conservative retries, pagination helpers, normalized rate-limit metadata, and lifecycle hooks.

Retry is enabled by default for GET, HEAD, PUT, DELETE, and OPTIONS. POST and PATCH are not retried automatically. Retryable failures include transport errors, HTTP 408, 425, 429, 5xx responses, and 403 responses that explicitly report an exhausted rate limit.

METHODS

new

my $api = HTTP::API::Core->new(
    base_url => 'https://api.example.com',
    headers  => { ... },
    timeout  => 10,
    retry    => { attempts => 3 },
    hooks    => { ... },
);

base_url is required. headers, timeout, retry, and hooks are optional. Retry defaults to three attempts with exponential backoff and jitter.

get, post, put, patch, delete

Convenience methods around request.

paginate

my $pager = $api->paginate(
    '/users',
    mode  => 'next_url',
    items => 'data.items',
    next  => 'links.next',
);

Returns an HTTP::API::Core::Pagination iterator. Supported modes are next_url, page, and cursor.

request

my $response = $api->request('POST', '/items', json => { ... });

Pass json to encode a Perl value as JSON, or content to send raw content. Pass query as a hash reference to append percent-encoded query parameters. Array-reference values produce repeated keys and undefined values are omitted. Per-request headers override default headers. Pass retry => 0 to disable retry for one request, or a retry hash to override the policy. A hooks hash can add request-local hooks after client-level hooks.

Non-2xx responses throw HTTP::API::Core::Error after retry is exhausted. Successful responses expose normalized rate-limit metadata through $response->rate_limit.

HOOKS

Hooks may be configured on the client or per request. Supported hook names are before_request, after_response, and on_error. Each value may be a coderef or an arrayref of coderefs.

before_request receives a mutable request context hash containing method, url, headers, content, and attempt. It runs immediately before each transport attempt. after_response receives the successful response object and request context. on_error receives the structured error and request context before retry is considered.

Request-local hooks run after client-level hooks. Hook failures are wrapped as non-retryable hook errors.

RETRY POLICY

The retry hash accepts attempts, base_delay, max_delay, jitter, and methods. Exponential backoff is capped by max_delay. A numeric Retry-After response header takes precedence over the calculated delay. When a 403 or 429 response reports an exhausted quota, RateLimit-Reset or X-RateLimit-Reset is used as a fallback delay when available.

RATE LIMITS

HTTP::API::Core::RateLimit normalizes RateLimit-*, X-RateLimit-*, and Retry-After response headers. It exposes limit, remaining, used, resource, reset metadata, exhausted, and wait_seconds.

ERROR HANDLING

Errors expose stable fields such as category, status, method, url, retryable, retry_after, request_id, and rate_limit. Hook failures use the hook category and are not retryable. Exact error message wording is not intended as a machine-readable API.

TRANSPORT CONTRACT

The transport constructor option accepts either a code reference or an object with a request method. Both are called as request($method, $url, \%options) and must return a hash reference containing at least status, with optional reason, headers, and content fields. Transport exceptions and invalid return values are normalized into structured transport errors. See docs/TRANSPORT.md for the extension contract.

IDEMPOTENCY

Pass idempotency => { key => ..., header => ... } to add an API-specific idempotency-key header to a request. The core deliberately does not assume a universal header name. An explicitly supplied request header with the same case-insensitive name takes precedence.

LICENSE

This library is free software; you may redistribute it and/or modify it under the same terms as Perl itself.