NAME
Punk::Request - a lazy wrapper over the PSGI environment
DESCRIPTION
Nothing is parsed until asked for, and everything parsed is cached on the object: query pairs, form bodies, cookies, the raw body. Reached through "req" in Punk::Context.
The object is a plain blessed array with a fixed slot layout and an all-C implementation - percent-decoding, pair splitting, multi-value promotion and header lookup without the regex engine; the body read runs through PerlIO and JSON decodes through File::Raw::JSON's C ABI. Load this module through Punk, which loads the compiled core first.
METHODS
new($env)
Constructed by the dispatcher; wraps the PSGI environment.
env
method
path
The raw environment, request method and path (/ when empty).
address
The client's address: REMOTE_ADDR. On a directly-exposed application that is the socket peer. Behind a reverse proxy with the proxy keyword in force it is the resolved client, because Punk rewrites the env key rather than adding a second one - see "proxy" in Punk. The connecting address is then available as $c->env->{'punk.peer_addr'}.
header($name)
A request header by name, case-insensitively (Content-Type and Content-Length included).
headers
Every request header as a hashref, keyed lowercase and dash-separated - x-forwarded-for, content-type, content-length - which is how HTTP/2 spells them and the same shape the OpenAPI validation path builds.
Reach for "header" when you know the name: it folds case, and a plain hash cannot, so $req->headers->{'X-Foo'} misses where $req->header('X-Foo') hits. This is for the cases where you want them all - logging, proxying, signing.
The hash is built fresh each call, so keeping or changing it is safe.
param($name)
Query parameter first, then form body parameter. A repeated parameter yields an arrayref.
params
params(@names)
With no names, one merged hashref, query winning over form.
With names, only those, looked up the way "param" looks one up. The return follows context: a list of values in the order asked for, undef for a name neither table has -
my ($page, $size) = $req->params(qw(page size));
or, in scalar context, a hashref holding only the names that were there, which is the shape to build a filter from -
my %filter = %{ $req->params(qw(state queue task)) };
A dereference block puts what it wraps in scalar context, so the %{ } above gets the hashref. Somewhere already in list context - an argument list, a hash constructor - it takes the slice instead, so ask for the hashref explicitly there with scalar.
Passing a list that happens to be empty is passing no names at all, and so gives everything: guard the call where the names are built at runtime.
query
form
The parsed body: application/x-www-form-urlencoded pairs, or the field parts of a multipart/form-data submission (whose file parts become uploads).
upload($name)
uploads
The Punk::Upload for a multipart file field - the first if several - and the { name => upload | [uploads] } hash of all of them. Both parse the body once.
body
The raw request body bytes (undef when there is none), read once and rewound.
json
The body decoded as JSON through File::Raw::JSON's C ABI.
body_each($code, %options)
my $bytes = $c->req->body_each(sub {
my ($chunk, $req) = @_;
$digest->add($chunk);
});
The body a window at a time instead of all at once. $code is called with each chunk and the request; the return value is the total byte count.
body copies the whole request into one scalar, which is right for JSON and wrong for anything large - the server is already holding those bytes, and the copy doubles them for as long as the handler runs. This is the same window the multipart parser has always read uploads through, for a body that is not multipart: an import, an application/octet-stream PUT, a feed of newline-delimited JSON.
Options: chunk, the window size in bytes (default 65536), and max, a ceiling after which the read croaks (default 0, no ceiling).
body_to($dest, %options)
my $bytes = $c->req->body_to('/var/spool/import.ndjson');
my $bytes = $c->req->body_to($fh, chunk => 1024 * 1024);
The body straight to a file - a path, which is opened and closed here, or a handle already open for writing. Nothing larger than one window is ever held in the application. Takes the same chunk and max options, and returns the byte count.
Reading the body once
A body can be read whole or in chunks, not both ways round. After body_each or body_to the bytes have gone, and body, json and form croak saying so rather than answering with nothing - an empty string where a body was expected is a bug that ships. The other order is not a trap: a body already read whole is replayed to body_each from the copy, so the order two pieces of code happen to run in cannot break either.
What happens with no CONTENT_LENGTH is HTTP's answer. With no transfer coding either, the request has no body and nothing is read. A chunked body has no declared length, so it is read to EOF - but only on a server that sets psgix.input.buffered, because reading to EOF on a live socket is how an application hangs. max is the only ceiling in that case: max_body had no length to check either.
cookies
cookie($name)
The request cookie jar (first value wins) / one cookie value.
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)