NAME

Catalyst::Seal::Prepare - the request preparation path

DESCRIPTION

Catalyst::prepare turns a PSGI environment into a request object. What that costs, measured by replacing each part with a stub that answers from a constant and timing the whole request, on a hello world application with phases 0 to 3 already applied:

prepare_query_parameters   4.3 us on an empty query string
                          34.0 us on "a=1&b=two&c=caf%C3%A9&d=one+two"
prepare_path               8.0 us
prepare_headers            7.3 us

A stub is the ceiling: no implementation of a subroutine beats not running it. Splitting the first of those again says where it goes, and it is not where the plan for this phase expected:

the whole unicode decoding step   23.7 us
  of which Try::Tiny              15.0 us
percent and plus unescaping        2.7 us

So the largest single item in request preparation is not parsing. It is that Catalyst decodes every parameter name and every parameter value inside a Try::Tiny block, which builds two closures and names them, per string.

What this module does

  • Catalyst::_handle_param_unicode_decoding, try rewritten as eval. Query parameters, body parameters and path arguments all decode through it, so it is paid once per string in the request.

  • Catalyst::Request::prepare_headers, building the HTTP::Headers hash directly instead of through 29 header calls.

  • Catalyst::Engine::prepare_path, skipping URI::canonical when the URI it just built is already canonical, which is decidable with one regex and costs three authority parses to ask URI.

The parameter decoder

Catalyst::_handle_param_unicode_decoding is phase 0.4 applied to a third site, with the same three things to preserve:

$@ is read immediately after the eval, and local $@ restores the caller's, which is what Try::Tiny does and a bare eval does not.

eval { ...; 1 } is not used here because the value of the block is the return value. @out distinguishes a failed decode from one that returned false, which testing $@ alone would not.

return unless defined $value returns the empty list in list context, and this subroutine is called from inside a map. It stays exactly as it is.

The headers

HTTP::Headers stores a header as $self->{lc $field}, plus an entry in $self->{'::std_case'} naming the spelling to use on the way out for any field it does not already know. Building that hash directly is a third of prepare_headers.

The spelling is not copied out of HTTP::Headers. Its list of known headers is a lexical, and a copy of it here would be wrong the day a header is added to it, in a way that only shows up in the as_string of a response. Instead the first request that carries a given environment key sets that one header on a throwaway HTTP::Headers the ordinary way and remembers what came out. The answer is HTTP::Headers' own, so there is nothing to keep in step, and the probe is also the check: a field whose result is not one plain string under one key is left to the stock path forever.

The memo is bounded. A client that sends a thousand distinct header names must not be able to grow it, so past the cap an unrecognised key takes the stock path and is not remembered.

The path

prepare_path builds the request URI as a string, blesses a reference to it into URI::http, and calls canonical. URI::_server::canonical parses the authority three times to decide whether anything needs canonicalising, and on a URI that is already canonical it returns the object it was given.

That decision is one regex here: a lower case scheme, no percent escape anywhere in the string, and an authority with no upper case and no port. Under those three conditions URI cannot find anything to change, and returns the same object this would.

The conditions are checked against URI itself at seal time rather than against its source, because what matters is the behaviour and not the spelling. A negative control is part of that: a probe that only ever confirms is a probe that would pass against a canonical that had stopped working.

fast_canonical

my $bool = Catalyst::Seal::Prepare::fast_canonical();

Whether the prepare_path patch was installed. For the test suite.

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)