NAME
Punk::Context - the per-request object
DESCRIPTION
Every guard, hook, plugin helper and controller receives one argument: the context. It wraps the PSGI environment lazily and carries the response builders. See Punk for the framework overview.
The class is entirely XS: storage is array slots and every method is an XSUB reading them directly, so the context costs nothing per request beyond its construction.
METHODS
env
The raw PSGI environment hashref.
app
The compiled Punk::App.
req
The lazy Punk::Request.
res
The lazy Punk::Response builder - only constructed when used.
ua
my $res = $c->ua->get($url)->get; # blocking
get '/proxy' => sub { # or hand the future back
my ($c) = @_;
$c->ua->get($url)->then(sub { $c->json({ got => $_[0]->content }) });
};
The outbound Fetch agent. Unlike the accessors above this is not per request: it is one agent per worker, shared by every request that worker serves, so its keep-alive pool and DNS state survive between them. The context only memoises the lookup. Configure it with the "ua" in Punk keyword.
On a Hyperman worker the agent runs on the same event loop serving inbound requests, so returning its future from a handler lets the worker answer others while the call is in flight. Anywhere else - a test, a script, punk console - there is no loop to join, Fetch uses its own, and ->get blocks.
Each worker builds its own on first use, so no two share a socket.
With ua cookie_jar => 1 this returns a per-request clone carrying its own jar, over the same pool. The context memoises whichever it is, so calling $c->ua twice in one request gets one agent and one jar. See Punk::UA.
stash
A per-request hashref for passing values between guards, hooks and the controller.
param($name)
Validated OpenAPI parameters first (path, then query), then web route captures, then the request (query, then form body).
params
params(@names)
The same layers, several names at a time. With no names, all of them merged into one hashref, stacked in that precedence.
With names, only those: a list of values in the order asked for (undef for a name no layer has) in list context, and in scalar context a hashref of just the names that were there, so a set of optional filters is one call rather than a loop -
my %filter = %{ $c->params(qw(state queue task worker)) };
Note that %{ } is scalar context, but an argument list is not: reach for scalar where the call sits somewhere already in list context. A list of names that happens to be empty is the same call as no names, and so gives everything. See "params" in Punk::Request, which this defers to for the last layer.
openapi
The validated parameter hash from "validate_request" in Open::API on API routes; undef elsewhere.
model($name)
The registered Punk::Model instance (per-worker, built on first access).
render($template, \%data, %options)
Render through the app's view engines; returns a finished response. See Punk::Views.
json($data, $status?)
text($body, $status?)
html($body, $status?)
redirect($url, $status?)
not_found
Finished responses. Status and headers previously set through "status" and "header" are folded in.
send_file($source, %options)
get '/invoice/:id' => sub {
my $c = shift;
return $c->send_file("/var/store/$id.pdf",
filename => "invoice-$id.pdf");
};
# bytes already in memory (a generated document)
return $c->send_file(\$pdf_bytes, type => 'application/pdf');
A finished download response, returned by the handler like any other. The source is a file path or a reference to a scalar of bytes. The whole download story is handled here: ETag (strong, from mtime and size) and Last-Modified with 304 answers to If-None-Match / If-Modified-Since; a single byte Range served as 206 with Content-Range (416 when unsatisfiable, and a multi-range or malformed header is legally answered with the full 200); If-Range honoured on an exact validator match; HEAD answered with the real headers and no body. Headers previously set through "header" are folded in. Ranged file bodies ride Punk::SendFile::Reader, so no more than 64KB of the file is in memory at once; a full-file body is a plain filehandle the server streams.
Options: type (Content-Type; otherwise inferred from the path or filename extension, else application/octet-stream), filename (sets Content-Disposition: attachment with the name, RFC 5987-encoded when it is not ASCII), inline (disposition inline instead), ranges => 0 (ignore Range and stop advertising Accept-Ranges), mtime / etag (override the validators; mtime is what gives a scalar source one), and missing => 'not_found' (answer the house 404 for an unreadable path instead of croaking).
The path is served as given - if any part of it came from the request, the traversal guard is yours.
respond_to(%format_handlers)
return $c->respond_to(
json => sub { $_[0]->json({ book => $book }) },
html => sub { $_[0]->render('book/view', { book => $book }) },
any => sub { $_[0]->text('book', 200) },
);
Accept negotiation: calls the handler for the most acceptable offered format and returns its response. Formats are json, html, text, xml or any full media type ('application/vnd.book+json'); q-values order the choice and q=0 excludes. A client that expressed no preference - no Accept, or only a wildcard match - gets the format its own request Content-Type names when that is offered, else the first registered. When nothing fits, the any handler is called if given; otherwise the response is a 406. Every outcome carries Vary: Accept.
status($code)
header($name => $value)
Set response status / add a response header; chainable. With no arguments status returns the pending status.
cookie($name)
cookie($name => $value, %opts)
With one argument, read a request cookie. With a value, set a Set-Cookie on the response (an undef value deletes it); options path (default /), domain, max_age, secure, httponly, samesite. The set form chains.
session
The signed cookie-backed session hashref (see Punk::Session); requires the session keyword. Read and write it; it is written back to the cookie at the end of the request if it changed.
session_expire
Log out: empty the session and delete its cookie. Chainable.
flash
$c->flash(notice => 'Saved.'); # set, for the NEXT request
my $note = $c->flash('notice'); # read this request's inbound
my $all = $c->flash; # the whole inbound hashref
One-request messages over the session (requires the session keyword): set with pairs (chainable), read by key, or take the whole inbound hashref for a template. See "FLASH" in Punk::Session for the lifecycle.
flash_keep
Re-arm this request's inbound flash for one more request. Chainable.
validate($schema?, $data?)
my $v = $c->validate(\%json_schema); # run a validation now
return $c->json({ errors => $v->errors }, 400) if $v->has_errors;
my $v = $c->validate; # no args: the last Result
Collecting request validation - never croaks on invalid data. With a schema, runs: $data defaults to the decoded JSON body for a JSON request, the merged params otherwise; returns a Punk::Validate Result. With no arguments, reads: the last Result this request produced (a route-level validate option ran before the handler), or undef.
login($user_or_id)
logout
auth_id
current_user
check_password($user, $password)
issue_token($user_id, $kind, $ttl)
take_token($token, @kinds)
The authentication battery's surface; all need the auth keyword. login records the identity in the session and logout expires it; auth_id is the raw session id, current_user the row loaded once per request through the configured model. check_password burns the same PBKDF2 work when there is no user or hash, so login timing reveals nothing. The token pair mints and spends single-use email tokens - spending deletes first, then validates. See Punk::Auth.
upload($name)
The Punk::Upload for a multipart/form-data file field (the first if several), via $c->req->upload.
log
The request Punk::Logger (cached for the request): $c->log->info(...), debug, warn, error, fatal. Its lines carry the request's method and path, and are delivered to the server's psgix.logger when one is present. Configure with the logging keyword. See Punk::Logger.
match
Routing information for the matched route (captures, route record).
promise
A new pending Punk::Future - loop-backed on a live Hyperman worker, self-contained (blocking) otherwise. Return it (or a then of it) from a handler to defer the response; settle it later from whatever wakes it.
get '/wait' => sub {
my ($c) = @_;
my $p = $c->promise;
$c->timer(1)->on_done(sub { $p->done($c->json({ ready => 1 })) });
return $p; # answered when $p is settled
};
timer($secs)
after($secs)
A Punk::Future that settles after $secs: a loop timer on a worker, a sleep off it. $c->timer(2)->then(sub { ... }) answers the request two seconds later without pinning the worker.
await($future)
Block until $future is ready and return its values (rethrowing a failure) - pumping the loop re-entrantly on a worker, blocking off it. The imperative escape hatch; return $future is the non-blocking way.
stash_hv
openapi_params
The raw storage slots behind "stash" and "openapi", read or written directly (the accessor pair the class is built from). Prefer stash and openapi, which lazily build and coerce; these exist for the framework and for code that wants the slot untouched.
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)