NAME
Punk::CORS - cross-origin resource sharing
SYNOPSIS
package MyApp;
use Punk;
cors; # a public API: * , no credentials
cors origins => [ 'https://app.example.com' ],
credentials => 1,
headers => [qw(Content-Type X-CSRF-Token)],
expose => [qw(X-Request-Id)],
max_age => 600,
paths => [ '/api' ];
DESCRIPTION
Answers preflights and adds the cross-origin headers, from inside the C dispatcher.
Both halves happen where they have to. A preflight is answered before routing, because OPTIONS on a path with no OPTIONS route would otherwise be the router's 405. Responses are decorated on the way out of the dispatcher rather than in an after-dispatch hook, because 404 and 405 never build a context and would otherwise go out bare - which is exactly when a browser most needs to be told it may read the status.
What it is not
CORS is not access control. It tells a browser what script on another origin may read; nothing else honours it, and curl has never cared. An endpoint that must not be reached by a given caller needs a guard, not a header.
In particular it is not a replacement for Punk::CSRF. A credentialed cross-origin POST that CORS permits still has to carry a live token. The two answer different questions - "may this script read the reply" and "did this request come from our own page" - and an application that takes cookies wants both.
THE KEYWORD
origins-'*', a single origin, an arrayref of exact origins, or a coderef called with the request's origin. The coderef may return true (echo the origin) or a replacement origin. Default'*'.credentials- sendAccess-Control-Allow-Credentials: true. Requires explicit origins: browsers refuse credentials with'*', and reflecting whatever origin arrives while allowing credentials is the classic CORS hole - every site gets to read your users' authenticated responses. The keyword croaks atto_apprather than let it be written.headers- theAccess-Control-Allow-Headerslist for preflights. Omitted, the requested headers are echoed.expose- response headers script may read beyond the safelisted ones.max_age- how long a preflight may be cached, in seconds (default 600).paths- path prefixes CORS applies to.paths => ['/api']is how an application serves HTML same-origin and its API cross-origin.methods- override the preflight's method list. See below for why you probably should not.
cors 0 turns it off. It also reads a cors: block from config/punk.yml.
Allow-Methods comes from the router
A preflight is answered with the methods that path actually serves, taken from the same routing table that produces Allow on a 405 - so it includes API operations and can never promise a method the application does not have. A preflight for a method the path does not answer is refused with 403 rather than waved through to fail later.
methods overrides this for the case the router cannot see: a mounted PSGI application whose own routes are opaque to Punk.
Vary
Vary: Origin goes on every response whose Allow-Origin depends on the request's origin - anything but a flat '*'. Without it a shared cache will happily serve one origin's response to another, which is a cache-poisoning bug wearing a CORS costume.
SEE ALSO
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)