<?xml version="1.0" encoding="UTF-8"?>
<rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom" xmlns:dc="http://purl.org/dc/elements/1.1/">
  <channel>
    <title>DEV Community: John Napiorkowski</title>
    <description>The latest articles on DEV Community by John Napiorkowski (@jjn1056).</description>
    <link>https://dev.to/jjn1056</link>
    <image>
      <url>https://media2.dev.to/dynamic/image/width=90,height=90,fit=cover,gravity=auto,format=auto/https:%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Fuser%2Fprofile_image%2F1119943%2F3492ab20-1496-43e6-a9b2-33f6a5adccae.jpeg</url>
      <title>DEV Community: John Napiorkowski</title>
      <link>https://dev.to/jjn1056</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/jjn1056"/>
    <language>en</language>
    <item>
      <title>CatalystNext: What Should We Keep, Change, and Rethink?</title>
      <dc:creator>John Napiorkowski</dc:creator>
      <pubDate>Tue, 28 Jul 2026 15:51:19 +0000</pubDate>
      <link>https://dev.to/jjn1056/catalystnext-what-should-we-keep-change-and-rethink-39kf</link>
      <guid>https://dev.to/jjn1056/catalystnext-what-should-we-keep-change-and-rethink-39kf</guid>
      <description>&lt;p&gt;Suppose we could redesign Catalyst without preserving every compatibility&lt;br&gt;
decision it has accumulated. Which parts would we keep because they still&lt;br&gt;
work? Which would we simplify? Which pain points have experienced Catalyst&lt;br&gt;
developers learned to work around, but should not be inherited by a new&lt;br&gt;
framework?&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;CatalystNext&lt;/strong&gt; is the working name for that redesign. This is not a v1&lt;br&gt;
proposal. I want to make the consequential choices visible while Catalyst&lt;br&gt;
users can still challenge them: subroutine attributes or a DSL, chained&lt;br&gt;
dispatch or another routing model, action and controller namespaces,&lt;br&gt;
request-scoped state, services and MVC, and the boundary between a small core&lt;br&gt;
and a complete framework.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://github.com/jjn1056/pagi" rel="noopener noreferrer"&gt;PAGI&lt;/a&gt;, the Perl Asynchronous Gateway&lt;br&gt;
Interface, would give CatalystNext an async-native execution foundation.&lt;br&gt;
Supporting APIs, WebSockets, streams, MCP, and other current workloads&lt;br&gt;
matters, but modernization is not the main question of this post. The main&lt;br&gt;
question is whether the proposed programming model addresses Catalyst's real&lt;br&gt;
strengths and pain points.&lt;/p&gt;
&lt;h2&gt;
  
  
  1. What should writing CatalystNext feel like?
&lt;/h2&gt;
&lt;h3&gt;
  
  
  Subroutine attributes, a route DSL, or conventions?
&lt;/h3&gt;

&lt;p&gt;Catalyst is, for better or worse, a subroutine-attribute framework. An action&lt;br&gt;
looks like a Perl subroutine with declarations attached to it. That keeps the&lt;br&gt;
route metadata beside the code that handles the route, and it is an important&lt;br&gt;
part of Catalyst's identity.&lt;/p&gt;

&lt;p&gt;There are other credible choices.&lt;/p&gt;

&lt;p&gt;A dedicated route DSL can show the whole application more clearly and avoid&lt;br&gt;
some of Perl's attribute machinery. Rails demonstrates how much can follow&lt;br&gt;
from a compact resource declaration:&lt;br&gt;
&lt;a href="https://guides.rubyonrails.org/routing.html" rel="noopener noreferrer"&gt;&lt;code&gt;resources :photos&lt;/code&gt;&lt;/a&gt; expands&lt;br&gt;
into the conventional collection and member routes, named helpers, and&lt;br&gt;
controller actions. At the other end, an explicit route table can make every&lt;br&gt;
method, path, and handler visible without any convention.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://fastapi.tiangolo.com/tutorial/path-params/" rel="noopener noreferrer"&gt;FastAPI&lt;/a&gt; takes another&lt;br&gt;
path: decorators remain close to the handler, while ordinary Python type&lt;br&gt;
declarations also drive parsing, validation, serialization, and generated&lt;br&gt;
OpenAPI documentation. The interesting part is not the Python syntax; it is&lt;br&gt;
how much useful application metadata becomes available from one declaration.&lt;/p&gt;

&lt;p&gt;My instinct is that CatalystNext should remain attribute-driven at its main&lt;br&gt;
user-facing surface. I am not in love with inventing a large new DSL, and I&lt;br&gt;
do not want a supposedly convenient route convention to conceal most of the&lt;br&gt;
application.&lt;/p&gt;

&lt;p&gt;Questions I would value feedback on:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Are subroutine attributes still a pleasant and idiomatic way to describe
actions in modern Perl?&lt;/li&gt;
&lt;li&gt;Is having route information beside the handler more valuable than seeing
the complete route tree in one file?&lt;/li&gt;
&lt;li&gt;Which declarations should remain explicit even if a Rails-like resource
shortcut is available?&lt;/li&gt;
&lt;li&gt;Would it be useful for attributes, a DSL, and conventions to be alternative
front ends over the same underlying application model, or would that merely
recreate Catalyst's “too many ways” problem?&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The declaration surface does not settle what those declarations build. That&lt;br&gt;
leads to the dispatch model itself.&lt;/p&gt;
&lt;h3&gt;
  
  
  What kind of dispatcher?
&lt;/h3&gt;

&lt;p&gt;I am strongly inclined to keep chained dispatch. Chains remain a good way to&lt;br&gt;
express the structure that real applications repeat:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;identify tenant
  → authorize user
    → load project
      → load issue
        → perform endpoint
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The problem is that Catalyst's historical chained syntax accumulated several&lt;br&gt;
ways to say similar things. Controller namespaces, action namespaces, path&lt;br&gt;
parts, captures, and private action names could all interact. People often&lt;br&gt;
found chaining powerful only after they had survived learning how its pieces&lt;br&gt;
were bolted together.&lt;/p&gt;

&lt;p&gt;We are not bound by that compatibility surface in a new framework. The goal&lt;br&gt;
would be to retain chains while separating three things that old Catalyst&lt;br&gt;
often allowed to blur together:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;Code origin&lt;/strong&gt; — the package and subroutine containing the code.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Logical action name&lt;/strong&gt; — the stable name used for chaining, diagnostics,
and reverse lookup.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;URL path&lt;/strong&gt; — the path used for HTTP dispatch.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;Here is one deliberately incomplete syntax experiment:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight perl"&gt;&lt;code&gt;&lt;span class="nb"&gt;package&lt;/span&gt; &lt;span class="nn"&gt;MyApp::Controller::&lt;/span&gt;&lt;span class="nv"&gt;AddressBook&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="c1"&gt;# URL: /contacts&lt;/span&gt;
&lt;span class="k"&gt;sub &lt;/span&gt;&lt;span class="nf"&gt;contacts&lt;/span&gt; &lt;span class="p"&gt;:Via('/') :At('contacts') {&lt;/span&gt;
    &lt;span class="c1"&gt;# Work shared by the collection.&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="c1"&gt;# URL: /contacts/{contact_id}&lt;/span&gt;
&lt;span class="k"&gt;sub &lt;/span&gt;&lt;span class="nf"&gt;contact&lt;/span&gt; &lt;span class="p"&gt;:Via('../contacts') :At('{&lt;/span&gt;&lt;span class="nv"&gt;contact_id:&lt;/span&gt;&lt;span class="o"&gt;\&lt;/span&gt;&lt;span class="nv"&gt;d&lt;/span&gt;&lt;span class="o"&gt;+&lt;/span&gt;&lt;span class="p"&gt;}'&lt;/span&gt;&lt;span class="s1"&gt;) {
    # Work shared by one contact.
}

# Matches: GET /contacts
sub list :Via(&lt;/span&gt;&lt;span class="p"&gt;'&lt;/span&gt;&lt;span class="o"&gt;../&lt;/span&gt;&lt;span class="nv"&gt;contacts&lt;/span&gt;&lt;span class="p"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;) :Get(&lt;/span&gt;&lt;span class="p"&gt;''&lt;/span&gt;&lt;span class="s1"&gt;) {
}

# Matches: GET /contacts/{contact_id}
sub show :Via(&lt;/span&gt;&lt;span class="p"&gt;'&lt;/span&gt;&lt;span class="o"&gt;../&lt;/span&gt;&lt;span class="nv"&gt;contact&lt;/span&gt;&lt;span class="p"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;) :Get(&lt;/span&gt;&lt;span class="p"&gt;''&lt;/span&gt;&lt;span class="s1"&gt;) {
}
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;In this experiment, &lt;code&gt;Via&lt;/code&gt; refers to a logical action and &lt;code&gt;At&lt;/code&gt; contributes a&lt;br&gt;
URL fragment. &lt;code&gt;Get($path)&lt;/code&gt; is simply &lt;code&gt;At($path)&lt;/code&gt; plus the HTTP &lt;code&gt;GET&lt;/code&gt; method.&lt;br&gt;
The same rule would apply to &lt;code&gt;Post&lt;/code&gt;, &lt;code&gt;Patch&lt;/code&gt;, and the other HTTP verbs.&lt;/p&gt;

&lt;p&gt;The logical name of &lt;code&gt;show&lt;/code&gt; is &lt;code&gt;/contacts/contact/show&lt;/code&gt;. Its URL is&lt;br&gt;
&lt;code&gt;/contacts/{contact_id}&lt;/code&gt;. Its Perl package is&lt;br&gt;
&lt;code&gt;MyApp::Controller::AddressBook&lt;/code&gt;. Those names are related only because the&lt;br&gt;
author chose them to be readable.&lt;/p&gt;

&lt;p&gt;This is a strawman, not a finished grammar. In particular, relative action&lt;br&gt;
references such as &lt;code&gt;Via('../contact')&lt;/code&gt; need to remain understandable when an&lt;br&gt;
application has several actions named &lt;code&gt;contact&lt;/code&gt;. Ambiguity should be a boot&lt;br&gt;
error, not a load-order rule.&lt;/p&gt;

&lt;p&gt;Rails-style resource dispatch has an obvious advantage here: most developers&lt;br&gt;
already know what &lt;code&gt;index&lt;/code&gt;, &lt;code&gt;show&lt;/code&gt;, &lt;code&gt;create&lt;/code&gt;, &lt;code&gt;update&lt;/code&gt;, and &lt;code&gt;destroy&lt;/code&gt; mean.&lt;br&gt;
Explicit route tables are also easier to explain than an action graph.&lt;br&gt;
Chaining has to earn its additional conceptual machinery.&lt;/p&gt;

&lt;p&gt;A second simplification may come from the execution model rather than the&lt;br&gt;
route grammar. If the terminal action returns a response, each intermediate&lt;br&gt;
action can be middleware around the rest of the chain: call the next action&lt;br&gt;
and return its response, transform or replace that response, or return a&lt;br&gt;
response immediately to short-circuit dispatch.&lt;/p&gt;

&lt;p&gt;This would be CatalystNext middleware over the context and response values,&lt;br&gt;
not raw PAGI middleware over event channels. It would give authorization,&lt;br&gt;
resource loading, and response decoration one composable shape. The separate&lt;br&gt;
question of whether action return values should acquire this meaning appears&lt;br&gt;
below.&lt;/p&gt;

&lt;p&gt;Questions:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Do chains still model something important that middleware, dependency
injection, or nested routers do not?&lt;/li&gt;
&lt;li&gt;Can a much smaller &lt;code&gt;Via&lt;/code&gt; plus &lt;code&gt;At&lt;/code&gt; vocabulary make chained dispatch
approachable?&lt;/li&gt;
&lt;li&gt;Does treating intermediate chain actions as response-value middleware make
the chain easier to understand?&lt;/li&gt;
&lt;li&gt;Should resourceful CRUD routes be a dispatcher of their own, a convenience
that produces a chain, or not part of the core?&lt;/li&gt;
&lt;li&gt;Should one application be allowed to combine dispatch strategies, or does a
framework need one blessed model?&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Even after choosing chains, the framework must decide where the names in those&lt;br&gt;
chains come from.&lt;/p&gt;
&lt;h3&gt;
  
  
  Should controller namespaces mean anything?
&lt;/h3&gt;

&lt;p&gt;My current leaning is that a controller exists for code organization only.&lt;br&gt;
Its package should tell the loader and the programmer where the code lives;&lt;br&gt;
it should not silently contribute a URL prefix or an action namespace.&lt;/p&gt;

&lt;p&gt;This will feel strange to people coming from frameworks where&lt;br&gt;
&lt;code&gt;Admin::UsersController&lt;/code&gt; naturally implies an &lt;code&gt;/admin/users&lt;/code&gt; area. It also&lt;br&gt;
removes an entire category of accidental coupling: reorganizing a large file&lt;br&gt;
into several controllers would not change routes, reverse-route names, or&lt;br&gt;
authorization behavior.&lt;/p&gt;

&lt;p&gt;The action's logical namespace would instead come from its place in the&lt;br&gt;
chain. A route dump would always show code origin, logical action, and URL&lt;br&gt;
together, so the separation is visible rather than magical.&lt;/p&gt;

&lt;p&gt;Questions:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Is a purely organizational controller liberating, or does it discard a
useful convention?&lt;/li&gt;
&lt;li&gt;When code origin and route structure differ, is a good route-inspection
command sufficient to keep the application comprehensible?&lt;/li&gt;
&lt;li&gt;Should an application be able to opt into package-derived defaults, even if
the framework's underlying model keeps the concepts separate?&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The authoring model tells us how actions are named and reached. The next layer&lt;br&gt;
is what enters, leaves, and flows between those actions.&lt;/p&gt;
&lt;h2&gt;
  
  
  2. What flows through the application?
&lt;/h2&gt;
&lt;h3&gt;
  
  
  Representations, content negotiation, and schemas
&lt;/h3&gt;

&lt;p&gt;For a long time, content negotiation promised that one resource could produce&lt;br&gt;
HTML, JSON, XML, or another representation based on the request. In practice,&lt;br&gt;
many applications created an HTML site and a separately versioned JSON API.&lt;br&gt;
JSON won a very large part of the API world, while server-rendered HTML has&lt;br&gt;
continued to evolve rather than vanish.&lt;/p&gt;

&lt;p&gt;Should one logical action still negotiate among representations? Or is&lt;br&gt;
&lt;code&gt;GET /contacts&lt;/code&gt; for HTML conceptually different from&lt;br&gt;
&lt;code&gt;GET /api/v1/contacts&lt;/code&gt; for JSON, even when both use the same underlying&lt;br&gt;
service?&lt;/p&gt;

&lt;p&gt;Schemas raise a related question. OpenAPI can document and generate clients&lt;br&gt;
for HTTP APIs. MCP tool definitions use JSON Schema for inputs and can declare&lt;br&gt;
schemas for structured outputs. A framework with a reliable model of action&lt;br&gt;
inputs and results could potentially generate much of this rather than asking&lt;br&gt;
the author to describe the same contract several times.&lt;/p&gt;

&lt;p&gt;But “generate the schema” can mean several incompatible things:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;infer a best-effort schema from Perl code;&lt;/li&gt;
&lt;li&gt;require typed request and response declarations;&lt;/li&gt;
&lt;li&gt;start with a schema and generate Perl-facing types;&lt;/li&gt;
&lt;li&gt;treat schemas as optional metadata which extensions may consume.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Questions:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Is content negotiation still a useful default, or should representations be
explicit actions?&lt;/li&gt;
&lt;li&gt;How much type and schema information will Perl developers realistically
maintain?&lt;/li&gt;
&lt;li&gt;Should validation, serialization, OpenAPI, and MCP schemas share one type
model?&lt;/li&gt;
&lt;li&gt;At what point does schema-driven development stop feeling like Perl?&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Schemas describe the public boundary of an action. Chained applications also&lt;br&gt;
need a disciplined way to move values across their internal boundaries.&lt;/p&gt;
&lt;h3&gt;
  
  
  Request state, services, models, and views
&lt;/h3&gt;

&lt;p&gt;Catalyst's stash was wonderfully flexible. That flexibility also made it easy&lt;br&gt;
for action chains and views to communicate through undocumented hash keys.&lt;/p&gt;

&lt;p&gt;I am considering a more structured replacement based on scoped attributes. A&lt;br&gt;
controller could declare an attribute with request scope. Once a chain action&lt;br&gt;
establishes its value, that value would be available to downstream&lt;br&gt;
controllers and to a called view.&lt;/p&gt;

&lt;p&gt;Conceptually:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight perl"&gt;&lt;code&gt;&lt;span class="nv"&gt;has&lt;/span&gt; &lt;span class="s"&gt;current_contact&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="s"&gt;scope&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;request&lt;/span&gt;&lt;span class="p"&gt;',&lt;/span&gt;
    &lt;span class="s"&gt;isa&lt;/span&gt;   &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="nv"&gt;Contact&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
&lt;span class="p"&gt;);&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;That could make the data flowing through a chain explicit and introspectable.&lt;br&gt;
It immediately creates hard questions:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Is the value write-once, replaceable, or lazily built?&lt;/li&gt;
&lt;li&gt;Is it visible only downstream or everywhere in the request?&lt;/li&gt;
&lt;li&gt;Who performs async construction and cleanup?&lt;/li&gt;
&lt;li&gt;Does a view receive it automatically?&lt;/li&gt;
&lt;li&gt;Should a model or domain service ever see request-scoped state implicitly,
or is that a layering violation?&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;There is a second naming problem here. “Model” has meant everything from a&lt;br&gt;
database handle to a domain object to an external API client. “View” can mean&lt;br&gt;
a template, serializer, response object, or negotiated representation.&lt;/p&gt;

&lt;p&gt;Perhaps the framework should fundamentally understand &lt;strong&gt;services&lt;/strong&gt; with&lt;br&gt;
lifetimes—application, request, connection—and allow models and views to be&lt;br&gt;
useful application-level categories layered on top. Or perhaps abandoning&lt;br&gt;
strong MVC vocabulary would discard one of Catalyst's most helpful organizing&lt;br&gt;
ideas.&lt;/p&gt;

&lt;p&gt;I would particularly like examples from larger applications: where did&lt;br&gt;
request-local state help, where did implicit availability cause trouble, and&lt;br&gt;
which resources genuinely needed application, request, or connection scope?&lt;/p&gt;

&lt;p&gt;These authoring and data-flow choices should address problems people have&lt;br&gt;
actually encountered in Catalyst applications. They also cannot assume that&lt;br&gt;
the applications we build today look exactly like the applications Catalyst&lt;br&gt;
was originally designed to serve.&lt;/p&gt;
&lt;h2&gt;
  
  
  3. What else must the redesign support?
&lt;/h2&gt;

&lt;p&gt;When Catalyst first appeared, the central use case was a database-backed,&lt;br&gt;
server-rendered website: templates, forms, sessions, and HTML responses.&lt;br&gt;
That remains important, but “web framework” now covers several workloads with&lt;br&gt;
related but non-identical needs:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;server-rendered HTML, forms, redirects, and sessions;&lt;/li&gt;
&lt;li&gt;JSON APIs and webhooks;&lt;/li&gt;
&lt;li&gt;bidirectional WebSockets and server-sent event streams;&lt;/li&gt;
&lt;li&gt;large streaming request and response bodies;&lt;/li&gt;
&lt;li&gt;MCP tools, resources, and prompts;&lt;/li&gt;
&lt;li&gt;possibly work that outlives one HTTP request.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;HTML did not disappear, but a new framework should not assume that every&lt;br&gt;
request ends by selecting a template.&lt;/p&gt;

&lt;p&gt;MCP makes the changed landscape especially visible. MCP's data layer is based&lt;br&gt;
on JSON-RPC and exposes primitives such as tools, resources, and prompts.&lt;br&gt;
Remote MCP commonly uses&lt;br&gt;
&lt;a href="https://modelcontextprotocol.io/docs/learn/architecture" rel="noopener noreferrer"&gt;Streamable HTTP&lt;/a&gt;,&lt;br&gt;
but an MCP tool is not naturally an HTTP path. Its identity, input schema,&lt;br&gt;
capabilities, progress notifications, and result types matter more than&lt;br&gt;
inventing a REST-shaped URL for it.&lt;/p&gt;

&lt;p&gt;Questions:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Are HTTP, WebSocket, SSE, and MCP several protocol front doors into one
application, or separate framework layers sharing services?&lt;/li&gt;
&lt;li&gt;Should MCP be a first-class CatalystNext concept or an add-on that consumes
the same metadata as HTTP APIs?&lt;/li&gt;
&lt;li&gt;What request or connection lifecycle is shared across these protocols?&lt;/li&gt;
&lt;li&gt;Where should long-running work stop being a controller action and become a
job or service?&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;These workloads do not need identical controller APIs, but they do need an&lt;br&gt;
execution foundation capable of supporting all of them. That is where PAGI&lt;br&gt;
enters the design.&lt;/p&gt;
&lt;h2&gt;
  
  
  4. PAGI as the foundation, not necessarily the user interface
&lt;/h2&gt;

&lt;p&gt;PAGI applications receive a connection scope plus asynchronous &lt;code&gt;receive&lt;/code&gt; and&lt;br&gt;
&lt;code&gt;send&lt;/code&gt; functions. This supports multiple incoming and outgoing events and&lt;br&gt;
makes backpressure visible.&lt;/p&gt;

&lt;p&gt;CatalystNext should take advantage of that foundation without forcing every&lt;br&gt;
ordinary database lookup or HTML response to look like low-level protocol&lt;br&gt;
code. “Async-native” needs to become a set of application-level guarantees:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;synchronous handlers remain straightforward;&lt;/li&gt;
&lt;li&gt;asynchronous handlers compose without blocking the server;&lt;/li&gt;
&lt;li&gt;cancellation reaches database queries, streams, and child work where
possible;&lt;/li&gt;
&lt;li&gt;request and connection resources are reliably cleaned up;&lt;/li&gt;
&lt;li&gt;streaming does not require bypassing the framework;&lt;/li&gt;
&lt;li&gt;dropping to raw PAGI remains possible at a defined boundary.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The controversial part is how much of that contract belongs in CatalystNext&lt;br&gt;
and how much belongs in PAGI-Tools, an event loop, or service implementations.&lt;/p&gt;
&lt;h3&gt;
  
  
  Should an action's return value mean something?
&lt;/h3&gt;

&lt;p&gt;In Catalyst, an action's return value has no framework meaning. An action&lt;br&gt;
changes the response attached to the context, and the dispatcher ignores&lt;br&gt;
whatever the subroutine returns. The introductory documentation demonstrates&lt;br&gt;
this by setting&lt;br&gt;
&lt;a href="https://metacpan.org/pod/Catalyst%3A%3AManual%3A%3AIntro" rel="noopener noreferrer"&gt;&lt;code&gt;$c-&amp;gt;res-&amp;gt;body&lt;/code&gt;&lt;/a&gt;&lt;br&gt;
without returning a response.&lt;/p&gt;

&lt;p&gt;That has confused people. Every Perl subroutine returns something, even&lt;br&gt;
without an explicit &lt;code&gt;return&lt;/code&gt;, yet &lt;code&gt;return $something&lt;/code&gt; from a Catalyst action&lt;br&gt;
has no effect on dispatch. The action's real result accumulates elsewhere on&lt;br&gt;
the context.&lt;/p&gt;

&lt;p&gt;There is a reasonable case for leaving this alone. A Catalyst action is a&lt;br&gt;
stage in a request lifecycle, not necessarily a function which produces a&lt;br&gt;
result. Several actions and a view can cooperate through the context, and an&lt;br&gt;
accidental final expression cannot unexpectedly become the response.&lt;/p&gt;

&lt;p&gt;But CatalystNext could make the terminal action's return value part of its&lt;br&gt;
contract. It would have to return an object which &lt;code&gt;isa PAGI::Response&lt;/code&gt;.&lt;br&gt;
CatalystNext could supply a &lt;code&gt;CatalystNext::Response&lt;/code&gt; subclass with framework&lt;br&gt;
conveniences while accepting other PAGI response subclasses.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://metacpan.org/pod/PAGI%3A%3AResponse" rel="noopener noreferrer"&gt;&lt;code&gt;PAGI::Response&lt;/code&gt;&lt;/a&gt; is a detached&lt;br&gt;
response value, so an action could be tested by inspecting its returned&lt;br&gt;
status, headers, and body without a live connection. Response ownership,&lt;br&gt;
redirects, errors, views, and chain short-circuits would all use the same&lt;br&gt;
explicit value. The dispatcher could reject &lt;code&gt;undef&lt;/code&gt; or an unrelated value&lt;br&gt;
instead of guessing whether some other code completed the response.&lt;/p&gt;

&lt;p&gt;The cost is a stricter and less forgiving endpoint contract. It may add&lt;br&gt;
ceremony, fit awkwardly with views which build a response in several steps,&lt;br&gt;
and turn Perl's harmless implicit return into a validation error.&lt;/p&gt;

&lt;p&gt;Should CatalystNext give an action's return value framework meaning, or is&lt;br&gt;
Catalyst's current behavior a useful consequence of its request lifecycle?&lt;/p&gt;

&lt;p&gt;Even with a response-value contract for ordinary endpoints, some applications&lt;br&gt;
will need direct ownership of the PAGI event channels. That is a separate&lt;br&gt;
boundary.&lt;/p&gt;
&lt;h3&gt;
  
  
  How PAGI-shaped should an action be?
&lt;/h3&gt;

&lt;p&gt;There is still a lower-level question underneath the response-value contract:&lt;br&gt;
should ordinary actions see only the CatalystNext chain described above, or&lt;br&gt;
should a terminal action be able to become a literal PAGI application and an&lt;br&gt;
intermediate action become literal PAGI middleware?&lt;/p&gt;

&lt;p&gt;&lt;a href="https://github.com/jjn1056/PAGI-Nano" rel="noopener noreferrer"&gt;PAGI::Nano&lt;/a&gt; provides a useful&lt;br&gt;
precedent. A Nano application is an ordinary PAGI application, but most HTTP&lt;br&gt;
handlers receive a friendly context and return a response value which Nano&lt;br&gt;
sends as PAGI events. Middleware uses the PAGI-shaped&lt;br&gt;
&lt;code&gt;($scope, $receive, $send, $next)&lt;/code&gt; contract. A &lt;code&gt;raw&lt;/code&gt; route can take ownership&lt;br&gt;
of its response and reach the underlying channels when it needs them.&lt;/p&gt;

&lt;p&gt;That gives Nano a “no silo, no cliff” property: application code does not pay&lt;br&gt;
the raw protocol cost for an ordinary JSON response, but it never becomes&lt;br&gt;
trapped above PAGI.&lt;/p&gt;

&lt;p&gt;CatalystNext could take a similar approach with an explicit &lt;code&gt;:Raw&lt;/code&gt; indicator&lt;br&gt;
(the name is only a strawman). The routing attributes are omitted from this&lt;br&gt;
example so we can focus on execution shape:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight perl"&gt;&lt;code&gt;&lt;span class="c1"&gt;# An ordinary endpoint returns a CatalystNext::Response.&lt;/span&gt;
&lt;span class="nv"&gt;async&lt;/span&gt; &lt;span class="k"&gt;sub &lt;/span&gt;&lt;span class="nf"&gt;show&lt;/span&gt; &lt;span class="p"&gt;($self, $c) {&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nv"&gt;$self&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="nv"&gt;view&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;show&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="s"&gt;contact&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="nv"&gt;$self&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="nv"&gt;current_contact&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="c1"&gt;# A raw intermediate action might receive the full middleware contract.&lt;/span&gt;
&lt;span class="nv"&gt;async&lt;/span&gt; &lt;span class="k"&gt;sub &lt;/span&gt;&lt;span class="nf"&gt;observe&lt;/span&gt; &lt;span class="err"&gt;:&lt;/span&gt;&lt;span class="nf"&gt;Raw&lt;/span&gt; &lt;span class="err"&gt;(&lt;/span&gt;
    &lt;span class="err"&gt;$&lt;/span&gt;&lt;span class="nf"&gt;self&lt;/span&gt;&lt;span class="err"&gt;,&lt;/span&gt; &lt;span class="err"&gt;$&lt;/span&gt;&lt;span class="nf"&gt;scope&lt;/span&gt;&lt;span class="err"&gt;,&lt;/span&gt; &lt;span class="err"&gt;$&lt;/span&gt;&lt;span class="nf"&gt;receive&lt;/span&gt;&lt;span class="err"&gt;,&lt;/span&gt; &lt;span class="err"&gt;$&lt;/span&gt;&lt;span class="nf"&gt;send&lt;/span&gt;&lt;span class="err"&gt;,&lt;/span&gt; &lt;span class="err"&gt;$&lt;/span&gt;&lt;span class="nf"&gt;next&lt;/span&gt;
&lt;span class="p"&gt;) {&lt;/span&gt;
    &lt;span class="k"&gt;my&lt;/span&gt; &lt;span class="nv"&gt;$observed_send&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nv"&gt;async&lt;/span&gt; &lt;span class="k"&gt;sub &lt;/span&gt;&lt;span class="p"&gt;($event) {&lt;/span&gt;
        &lt;span class="nv"&gt;$self&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="nv"&gt;metrics&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="nv"&gt;increment&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nv"&gt;$event&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="nv"&gt;type&lt;/span&gt;&lt;span class="p"&gt;});&lt;/span&gt;
        &lt;span class="nv"&gt;await&lt;/span&gt; &lt;span class="nv"&gt;$send&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nv"&gt;$event&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
    &lt;span class="p"&gt;};&lt;/span&gt;

    &lt;span class="nv"&gt;await&lt;/span&gt; &lt;span class="nv"&gt;$next&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nv"&gt;$scope&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nv"&gt;$receive&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nv"&gt;$observed_send&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="c1"&gt;# A raw endpoint owns its response and can emit any PAGI event.&lt;/span&gt;
&lt;span class="nv"&gt;async&lt;/span&gt; &lt;span class="k"&gt;sub &lt;/span&gt;&lt;span class="nf"&gt;feed&lt;/span&gt; &lt;span class="p"&gt;:Raw ($self, $c) {&lt;/span&gt;
    &lt;span class="k"&gt;my&lt;/span&gt; &lt;span class="nv"&gt;$emit&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nv"&gt;$c&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="nv"&gt;raw_send&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
    &lt;span class="nv"&gt;await&lt;/span&gt; &lt;span class="nv"&gt;$emit&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt; &lt;span class="s"&gt;type&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;contacts.feed.start&lt;/span&gt;&lt;span class="p"&gt;'&lt;/span&gt; &lt;span class="p"&gt;});&lt;/span&gt;
    &lt;span class="nv"&gt;await&lt;/span&gt; &lt;span class="nv"&gt;$emit&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;
        &lt;span class="s"&gt;type&lt;/span&gt;       &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;contacts.updated&lt;/span&gt;&lt;span class="p"&gt;',&lt;/span&gt;
        &lt;span class="s"&gt;contact_id&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="mi"&gt;42&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="p"&gt;});&lt;/span&gt;
    &lt;span class="nv"&gt;await&lt;/span&gt; &lt;span class="nv"&gt;$emit&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt; &lt;span class="s"&gt;type&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;contacts.feed.end&lt;/span&gt;&lt;span class="p"&gt;'&lt;/span&gt; &lt;span class="p"&gt;});&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Here &lt;code&gt;:Raw&lt;/code&gt; changes execution ownership, not routing. On a terminal action it&lt;br&gt;
means that the action must complete the response or protocol exchange. On an&lt;br&gt;
intermediate action it could mean that the action receives the full&lt;br&gt;
around-middleware contract. That second shape can run before and after the rest&lt;br&gt;
of the chain, replace or wrap &lt;code&gt;receive&lt;/code&gt; and &lt;code&gt;send&lt;/code&gt;, short-circuit dispatch, and&lt;br&gt;
observe errors. It operates at a lower level than ordinary response-value&lt;br&gt;
middleware, which sees the CatalystNext context, &lt;code&gt;next&lt;/code&gt;, and the response&lt;br&gt;
returned by the rest of the chain without taking ownership of the event&lt;br&gt;
channels.&lt;/p&gt;

&lt;p&gt;Keeping the PAGI event channels reachable also creates an extension seam&lt;br&gt;
which response values alone cannot provide. A handler might emit a semantic&lt;br&gt;
custom event such as &lt;code&gt;contacts.updated&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;A surrounding middleware could observe that event for metrics or tracing,&lt;br&gt;
reject it, enrich it, or translate it into SSE, NDJSON, or some future wire&lt;br&gt;
protocol. The handler can speak in application events while the wrapper owns&lt;br&gt;
the representation. PAGI::Nano already&lt;br&gt;
&lt;a href="https://github.com/jjn1056/PAGI-Nano/blob/main/examples/custom-send-events/app.pl" rel="noopener noreferrer"&gt;demonstrates this pattern&lt;/a&gt;&lt;br&gt;
with custom send events rendered into more than one wire format.&lt;br&gt;
Whether multiple wire formats should share one logical action is the&lt;br&gt;
content-negotiation question discussed above.&lt;/p&gt;

&lt;p&gt;The risk is making every CatalystNext action reason in transport-level terms.&lt;br&gt;
Raw &lt;code&gt;receive&lt;/code&gt; and &lt;code&gt;send&lt;/code&gt; are flexible, but they also expose event ordering,&lt;br&gt;
response ownership, awaiting every send, and protocol-specific failure modes.&lt;br&gt;
The framework is supposed to design those footguns out of ordinary application&lt;br&gt;
code.&lt;/p&gt;

&lt;p&gt;So the question may not be whether CatalystNext is built from PAGI apps and&lt;br&gt;
middleware—it almost certainly will be—but where that fact becomes visible:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Should every endpoint and intermediate action directly implement a PAGI
contract?&lt;/li&gt;
&lt;li&gt;Should ordinary actions receive a CatalystNext context and compile into PAGI
apps or middleware, with &lt;code&gt;:Raw&lt;/code&gt; as the explicit ownership escape hatch?&lt;/li&gt;
&lt;li&gt;Can &lt;code&gt;:Raw&lt;/code&gt; sensibly mean terminal PAGI application on an endpoint and PAGI
middleware on an intermediate action, or are those different concepts which
deserve different indicators?&lt;/li&gt;
&lt;li&gt;Should every ordinary intermediate chain action receive &lt;code&gt;next&lt;/code&gt; and behave
as response-value middleware, or should the framework also have simpler
one-way setup stages?&lt;/li&gt;
&lt;li&gt;Should custom event types be registered in the metamodel so extensions and
tooling can discover them, or remain an intentionally open convention?&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;PAGI determines what CatalystNext can execute, but it does not determine the&lt;br&gt;
right size of the framework or how much of Catalyst's existing shape a&lt;br&gt;
redesign should preserve. Those are product decisions rather than protocol&lt;br&gt;
decisions.&lt;/p&gt;
&lt;h2&gt;
  
  
  5. Where is the framework boundary?
&lt;/h2&gt;
&lt;h3&gt;
  
  
  How much compatibility should the name “Catalyst” imply?
&lt;/h3&gt;

&lt;p&gt;I do not want to preserve awkward syntax solely because Catalyst had to remain&lt;br&gt;
compatible with itself. A clean design needs permission to remove duplicate&lt;br&gt;
spellings and historical namespace behavior.&lt;/p&gt;

&lt;p&gt;At the same time, a framework called CatalystNext creates a reasonable&lt;br&gt;
expectation that Catalyst developers can recognize its concepts and migrate&lt;br&gt;
incrementally. PAGI can host existing PSGI applications through an adapter,&lt;br&gt;
which creates possibilities for gradual migration at the application&lt;br&gt;
boundary. It does not automatically provide source compatibility inside a&lt;br&gt;
controller.&lt;/p&gt;

&lt;p&gt;What matters most to potential users: familiar concepts, compatible&lt;br&gt;
controller code, coexistence with an old application, migration tooling, or&lt;br&gt;
simply a clear explanation of the break?&lt;/p&gt;

&lt;p&gt;Even with permission to break old syntax, we still need to decide how much&lt;br&gt;
belongs in the new core.&lt;/p&gt;
&lt;h3&gt;
  
  
  Small core or complete framework?
&lt;/h3&gt;

&lt;p&gt;There is a perennial framework argument underneath all of this: should the&lt;br&gt;
core be small and composable, or should a new application receive a coherent&lt;br&gt;
answer for routing, configuration, services, validation, sessions,&lt;br&gt;
authentication, rendering, errors, testing, and deployment?&lt;/p&gt;

&lt;p&gt;A tiny core is easier to understand and less likely to freeze yesterday's&lt;br&gt;
choices. It can also leave every application assembling a subtly incompatible&lt;br&gt;
stack. A batteries-included framework creates shared vocabulary and better&lt;br&gt;
out-of-the-box tooling, but every additional blessed subsystem raises the&lt;br&gt;
cost of maintenance and makes alternative choices feel second-class.&lt;/p&gt;

&lt;p&gt;My leaning is that CatalystNext should be &lt;strong&gt;small but complete and flexible&lt;/strong&gt;.&lt;br&gt;
That was one of Catalyst's strengths. It supplied enough shared structure for&lt;br&gt;
an application to feel like one system, without insisting that every useful&lt;br&gt;
idea had to live inside the core distribution.&lt;/p&gt;

&lt;p&gt;“Complete” does not mean incorporating every fashionable application model.&lt;br&gt;
It means defining a coherent application model, lifecycle, routing and&lt;br&gt;
dispatch contract, service boundary, error path, and extension surface.&lt;br&gt;
People should then be able to build more opinionated experiences on top:&lt;br&gt;
Livewire- or LiveView-like stateful UI frameworks, API-focused stacks, MCP&lt;br&gt;
tooling, or ideas none of us have named yet.&lt;/p&gt;

&lt;p&gt;That still leaves a difficult boundary. Is schema generation a layer? Are&lt;br&gt;
sessions? Are HTML views? Does authentication belong in the action model, in&lt;br&gt;
middleware, or entirely in application code? How much must the core&lt;br&gt;
standardize so independently developed extensions compose rather than merely&lt;br&gt;
coexist?&lt;/p&gt;

&lt;p&gt;I would like to hear which facilities must work together on day one for&lt;br&gt;
CatalystNext to deserve the word “framework,” and which should remain&lt;br&gt;
replaceable integrations.&lt;/p&gt;

&lt;p&gt;Being both complete and extensible requires a shared structural vocabulary.&lt;br&gt;
That is what keeps pulling me toward a metamodel rather than a collection of&lt;br&gt;
unrelated hooks.&lt;/p&gt;
&lt;h2&gt;
  
  
  My current architectural leaning: build around a metamodel
&lt;/h2&gt;

&lt;p&gt;The idea I keep returning to is a standalone application metamodel: something&lt;br&gt;
structural and extensible, somewhat MOP-like in spirit but not based on Moose&lt;br&gt;
and not limited to implementing CatalystNext.&lt;/p&gt;

&lt;p&gt;It might contain concepts along these lines:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Meta::App
  Meta::Controller
    Meta::Action
  Meta::Dispatcher
  Meta::Service
  Meta::View
  Meta::Attribute
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The exact class list is not the important part yet. The important separation&lt;br&gt;
is between &lt;strong&gt;describing an application&lt;/strong&gt; and &lt;strong&gt;turning that description into a&lt;br&gt;
running PAGI app&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;Subroutine attributes could build the model. A dedicated DSL or a&lt;br&gt;
resource-routing convention could also build the model. Dispatchers could&lt;br&gt;
compile actions into executable routing graphs. Extensions could inspect or&lt;br&gt;
contribute metadata for OpenAPI, MCP, authorization, or developer tooling.&lt;br&gt;
An application could fail at boot when routes collide or a chain consumes a&lt;br&gt;
value nobody provides.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://docs.spring.io/spring-framework/reference/web/webflux.html" rel="noopener noreferrer"&gt;Spring WebFlux&lt;/a&gt;&lt;br&gt;
is one useful precedent for separating these concerns: it supports both&lt;br&gt;
annotated controllers and functional endpoint routing over the same reactive&lt;br&gt;
web foundation. That does not mean CatalystNext should expose every possible&lt;br&gt;
frontend. “They all compile to metadata” is an architectural capability, not&lt;br&gt;
an excuse to avoid choosing one good default.&lt;/p&gt;

&lt;p&gt;My hope is that the metamodel gives CatalystNext a sane, stable, introspectable&lt;br&gt;
API for extension. Routes, actions, dispatchers, services, views, and scoped&lt;br&gt;
attributes should be discoverable through documented objects rather than by&lt;br&gt;
reverse-engineering controller packages or mutating framework internals.&lt;br&gt;
Extensions should be able to contribute metadata, validate the application&lt;br&gt;
model, and participate in compilation at defined points, while CatalystNext&lt;br&gt;
still provides one opinionated default experience.&lt;/p&gt;

&lt;p&gt;The metamodel also should not become a stealth decision about how application&lt;br&gt;
objects are written. Ideally plain Perl, Moo, Moose, Object::Pad, and core&lt;br&gt;
&lt;code&gt;class&lt;/code&gt; code could participate through a structural contract. I am less sure&lt;br&gt;
how far that neutrality can go before useful features become lowest-common-&lt;br&gt;
denominator abstractions.&lt;/p&gt;

&lt;p&gt;This is still only a direction. A metamodel can make routes inspectable and&lt;br&gt;
extensions cleaner, but it can also become an abstract architecture project&lt;br&gt;
that never produces a pleasant application framework. The user-facing&lt;br&gt;
experience has to come first.&lt;/p&gt;

&lt;h2&gt;
  
  
  What feedback would help
&lt;/h2&gt;

&lt;p&gt;I am not looking for a vote on whether every idea above is good. I would most&lt;br&gt;
value reports from experience: what hurt, what scaled, and what you would not&lt;br&gt;
want a redesign to lose.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Which Catalyst behavior proved valuable in a large application, even if its
syntax was confusing?&lt;/li&gt;
&lt;li&gt;Which Catalyst pain points have you learned to tolerate or work around?&lt;/li&gt;
&lt;li&gt;Which routing model do you find easiest to understand six months after
writing it?&lt;/li&gt;
&lt;li&gt;Should an action's return value have framework meaning? If terminal actions
return a &lt;code&gt;PAGI::Response&lt;/code&gt;, does treating intermediate chain actions as
middleware make chains clearer?&lt;/li&gt;
&lt;li&gt;Where have framework conventions saved work, and where have they hidden too
much?&lt;/li&gt;
&lt;li&gt;How have you successfully handled request-scoped values and async resource
lifetimes?&lt;/li&gt;
&lt;li&gt;Would typed request and response declarations earn their cost through
validation, OpenAPI, and MCP integration?&lt;/li&gt;
&lt;li&gt;What kinds of Perl web applications are you building—or would you build if
the framework support existed?&lt;/li&gt;
&lt;li&gt;Where has direct access to an async protocol or custom event stream been
useful, and where did it leak too much machinery into application code?&lt;/li&gt;
&lt;li&gt;Which facilities must be integrated before a framework is meaningfully more
useful than a router and middleware collection?&lt;/li&gt;
&lt;li&gt;What would a Livewire- or LiveView-like layer need from the core in order to
remain an extension rather than a fork?&lt;/li&gt;
&lt;li&gt;Should CatalystNext impose an object system, favor one without requiring it,
or remain completely structurally typed?&lt;/li&gt;
&lt;li&gt;Does a reusable metamodel sound like a useful foundation, or like premature
generalization?&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Please feel free to answer one narrow part. A detailed objection to one&lt;br&gt;
assumption is more useful at this stage than general encouragement.&lt;/p&gt;

&lt;p&gt;I have spent enough time trying to imagine the perfect v1 in isolation. I&lt;br&gt;
would rather find the shape of the problem with the community before turning&lt;br&gt;
these experiments into promises.&lt;/p&gt;

</description>
      <category>perl</category>
      <category>catalyst</category>
      <category>pagi</category>
    </item>
    <item>
      <title>Perl PAGI Middleware</title>
      <dc:creator>John Napiorkowski</dc:creator>
      <pubDate>Sun, 28 Jun 2026 00:20:24 +0000</pubDate>
      <link>https://dev.to/jjn1056/perl-pagi-middleware-o5b</link>
      <guid>https://dev.to/jjn1056/perl-pagi-middleware-o5b</guid>
      <description>&lt;h1&gt;
  
  
  Middleware in PAGI
&lt;/h1&gt;

&lt;p&gt;A port of the sample app from &lt;a href="https://theweeklychallenge.org/blog/what-is-middleware/" rel="noopener noreferrer"&gt;&lt;em&gt;What Is Middleware?&lt;/em&gt;&lt;/a&gt; — which builds the same three-layer stack in Plack/PSGI (Perl) and Starlette/ASGI (Python) — to &lt;strong&gt;PAGI&lt;/strong&gt;, an async, ASGI-style application interface for Perl.&lt;/p&gt;

&lt;p&gt;The app is deliberately tiny but exercises the three things middleware exists to do:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;Logger&lt;/strong&gt; — wrap the request, time it, log method/path in and status/duration out.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Authenticator&lt;/strong&gt; — inspect a header, &lt;em&gt;inject context&lt;/em&gt; for downstream layers on success, or &lt;em&gt;short-circuit&lt;/em&gt; with a 401 on failure.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;ProfileRouter&lt;/strong&gt; — answer one specific route from inside the stack, reading the context the Authenticator injected.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;All code below was run under &lt;code&gt;perl-5.40.0&lt;/code&gt; with &lt;code&gt;PAGI::Test::Client&lt;/code&gt;; the log lines and responses shown in Running it are the actual captured output, not hand-written.&lt;/p&gt;




&lt;h2&gt;
  
  
  The PAGI middleware contract
&lt;/h2&gt;

&lt;p&gt;A PAGI &lt;em&gt;application&lt;/em&gt; is, in the spec's words, "a single coderef returning a Future": an async sub over the &lt;code&gt;($scope, $receive, $send)&lt;/code&gt; triple — the same shape as ASGI. &lt;code&gt;$scope&lt;/code&gt; is the per-connection metadata hash (&lt;code&gt;type&lt;/code&gt;, &lt;code&gt;method&lt;/code&gt;, &lt;code&gt;path&lt;/code&gt;, &lt;code&gt;headers&lt;/code&gt;, …), &lt;code&gt;$receive&lt;/code&gt; pulls inbound events, &lt;code&gt;$send&lt;/code&gt; pushes outbound ones (&lt;code&gt;http.response.start&lt;/code&gt;, then &lt;code&gt;http.response.body&lt;/code&gt;), and the Future it returns &lt;em&gt;resolving&lt;/em&gt; is what tells the server the response is complete.&lt;/p&gt;

&lt;p&gt;&lt;em&gt;Middleware&lt;/em&gt; is just as plain: a subroutine that takes an application and returns a new application, wrapping the inner one. That is the whole spec-level contract — app in, app out:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight perl"&gt;&lt;code&gt;&lt;span class="k"&gt;sub &lt;/span&gt;&lt;span class="nf"&gt;middleware&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;my&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nv"&gt;$app&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nv"&gt;@_&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nv"&gt;async&lt;/span&gt; &lt;span class="k"&gt;sub &lt;/span&gt;&lt;span class="p"&gt;($scope, $receive, $send) {&lt;/span&gt;
        &lt;span class="c1"&gt;# ... before ...&lt;/span&gt;
        &lt;span class="nv"&gt;await&lt;/span&gt; &lt;span class="nv"&gt;$app&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nv"&gt;$scope&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nv"&gt;$receive&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nv"&gt;$send&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;   &lt;span class="c1"&gt;# call the inner app&lt;/span&gt;
        &lt;span class="c1"&gt;# ... after ...&lt;/span&gt;
    &lt;span class="p"&gt;};&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;A middleware propagates the inner app's Future — its completion and any exception flow straight through — and never reads its return value, which the spec defines as inert; to observe or rewrite the response it wraps &lt;code&gt;$send&lt;/code&gt; instead, and to add per-request context it clones &lt;code&gt;$scope&lt;/code&gt; (top-level edits stay visible downward only).&lt;/p&gt;

&lt;p&gt;&lt;code&gt;PAGI::Middleware&lt;/code&gt;, from PAGI-Tools rather than the spec, is a thin convenience layer over exactly that contract. Instead of a bare &lt;code&gt;sub&lt;/code&gt; you get a small class whose &lt;code&gt;wrap($app)&lt;/code&gt; returns the same app-to-app coderef:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight perl"&gt;&lt;code&gt;&lt;span class="k"&gt;sub &lt;/span&gt;&lt;span class="nf"&gt;wrap&lt;/span&gt; &lt;span class="p"&gt;($self, $app) {&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nv"&gt;async&lt;/span&gt; &lt;span class="k"&gt;sub &lt;/span&gt;&lt;span class="p"&gt;($scope, $receive, $send) {&lt;/span&gt;
        &lt;span class="c1"&gt;# ... before ...&lt;/span&gt;
        &lt;span class="nv"&gt;await&lt;/span&gt; &lt;span class="nv"&gt;$app&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nv"&gt;$scope&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nv"&gt;$receive&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nv"&gt;$send&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;   &lt;span class="c1"&gt;# call the inner app&lt;/span&gt;
        &lt;span class="c1"&gt;# ... after ...&lt;/span&gt;
    &lt;span class="p"&gt;};&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;It earns its keep mainly through &lt;code&gt;PAGI::Middleware::Builder&lt;/code&gt;: you get a &lt;code&gt;new&lt;/code&gt;/&lt;code&gt;_init&lt;/code&gt; config constructor, the &lt;code&gt;modify_scope&lt;/code&gt; / &lt;code&gt;intercept_send&lt;/code&gt; helpers, and a &lt;code&gt;wrap&lt;/code&gt; method the builder's &lt;code&gt;enable&lt;/code&gt; knows how to call. The three middleware below all take this form — but it's sugar over the plain subroutine above, not a different thing.&lt;/p&gt;

&lt;p&gt;From that single seam you get every middleware behaviour:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Behaviour&lt;/th&gt;
&lt;th&gt;How&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Observe the response&lt;/td&gt;
&lt;td&gt;wrap &lt;code&gt;$send&lt;/code&gt; in your own async sub and watch the events flow by&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Inject per-request context&lt;/td&gt;
&lt;td&gt;
&lt;code&gt;$self-&amp;gt;modify_scope($scope, { key =&amp;gt; $value })&lt;/code&gt; and pass the copy down&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Short-circuit&lt;/td&gt;
&lt;td&gt;render your own response with &lt;code&gt;$send&lt;/code&gt; and &lt;strong&gt;don't&lt;/strong&gt; call &lt;code&gt;$app&lt;/code&gt;
&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Pass non-HTTP through&lt;/td&gt;
&lt;td&gt;check &lt;code&gt;$scope-&amp;gt;{type}&lt;/code&gt; and delegate untouched&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;Two notes on how this maps from the original article:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Context injection.&lt;/strong&gt; PSGI mutates &lt;code&gt;$env-&amp;gt;{'custom.user_id'}&lt;/code&gt;; Starlette sets &lt;code&gt;request.state.user_id&lt;/code&gt;. PAGI uses &lt;code&gt;modify_scope&lt;/code&gt;, which &lt;em&gt;shallow-copies&lt;/em&gt; the scope and merges your additions, then hands the copy to the inner app. The data is visible to everything downstream but never leaks back up to outer middleware — injection without global mutation.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Responses are values.&lt;/strong&gt; &lt;code&gt;PAGI::Response-&amp;gt;json($data)&lt;/code&gt; builds a response object you can pass around; &lt;code&gt;-&amp;gt;respond($send)&lt;/code&gt; is what actually writes it to the connection. Short-circuiting is just "build a response and respond, skip &lt;code&gt;$app&lt;/code&gt;."&lt;/li&gt;
&lt;/ul&gt;

&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;Why &lt;code&gt;wrap($app)&lt;/code&gt; as the base shape?&lt;/strong&gt; The Starlette version subclasses &lt;code&gt;BaseHTTPMiddleware&lt;/code&gt; and overrides &lt;code&gt;dispatch(request, call_next)&lt;/code&gt; — which reads beautifully: you &lt;code&gt;await call_next(request)&lt;/code&gt; and get a &lt;code&gt;Response&lt;/code&gt; back. The ergonomics are lovely; the trouble is &lt;em&gt;how Starlette implements &lt;code&gt;call_next&lt;/code&gt;&lt;/em&gt;. It runs the inner app in a separate task feeding an in-memory stream, and that plumbing is what adds overhead and is known to trip over streaming responses, background tasks, and context propagation. It's also HTTP-only — anything touching WebSocket or lifespan traffic, or transforming a streaming body, has to drop to the raw ASGI form regardless. So PAGI makes the raw form — &lt;code&gt;wrap($app)&lt;/code&gt; over &lt;code&gt;($scope, $receive, $send)&lt;/code&gt; — the &lt;em&gt;substrate&lt;/em&gt; every middleware is built on: it can express everything, with no hidden task or stream in the path. A &lt;code&gt;call_next&lt;/code&gt;-style value-passing convenience can be layered on top of that substrate without inheriting the plumbing; what you don't want is to make the lossy, HTTP-only version the foundation.&lt;/p&gt;
&lt;/blockquote&gt;




&lt;h2&gt;
  
  
  Logger middleware
&lt;/h2&gt;

&lt;p&gt;Times the request across the inner app and logs a line on the way in and on the way out. To learn the final status, it wraps &lt;code&gt;$send&lt;/code&gt; and remembers the &lt;code&gt;status&lt;/code&gt; off the &lt;code&gt;http.response.start&lt;/code&gt; event.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight perl"&gt;&lt;code&gt;&lt;span class="nb"&gt;package&lt;/span&gt; &lt;span class="nn"&gt;MyApp::Middleware::&lt;/span&gt;&lt;span class="nv"&gt;Logger&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="k"&gt;use&lt;/span&gt; &lt;span class="nv"&gt;v5&lt;/span&gt;&lt;span class="mf"&gt;.40&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="k"&gt;use&lt;/span&gt; &lt;span class="nv"&gt;parent&lt;/span&gt; &lt;span class="p"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;PAGI::Middleware&lt;/span&gt;&lt;span class="p"&gt;';&lt;/span&gt;
&lt;span class="k"&gt;use&lt;/span&gt; &lt;span class="nn"&gt;Future::&lt;/span&gt;&lt;span class="nv"&gt;AsyncAwait&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="k"&gt;use&lt;/span&gt; &lt;span class="nn"&gt;Time::&lt;/span&gt;&lt;span class="nv"&gt;HiRes&lt;/span&gt; &lt;span class="sx"&gt;qw(time)&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="k"&gt;sub &lt;/span&gt;&lt;span class="nf"&gt;wrap&lt;/span&gt; &lt;span class="p"&gt;($self, $app) {&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nv"&gt;async&lt;/span&gt; &lt;span class="k"&gt;sub &lt;/span&gt;&lt;span class="p"&gt;($scope, $receive, $send) {&lt;/span&gt;

        &lt;span class="c1"&gt;# Pass through anything that isn't an HTTP request&lt;/span&gt;
        &lt;span class="c1"&gt;# (lifespan, websocket, sse) untouched.&lt;/span&gt;
        &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nv"&gt;await&lt;/span&gt; &lt;span class="nv"&gt;$app&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nv"&gt;$scope&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nv"&gt;$receive&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nv"&gt;$send&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
          &lt;span class="k"&gt;unless&lt;/span&gt; &lt;span class="nv"&gt;$scope&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="nv"&gt;type&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="ow"&gt;eq&lt;/span&gt; &lt;span class="p"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;http&lt;/span&gt;&lt;span class="p"&gt;';&lt;/span&gt;

        &lt;span class="k"&gt;my&lt;/span&gt; &lt;span class="nv"&gt;$start_time&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nb"&gt;time&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
        &lt;span class="nv"&gt;say&lt;/span&gt; &lt;span class="p"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;[LOG] Incoming: &lt;/span&gt;&lt;span class="si"&gt;$scope&lt;/span&gt;&lt;span class="s2"&gt;-&amp;gt;{method} &lt;/span&gt;&lt;span class="si"&gt;$scope&lt;/span&gt;&lt;span class="s2"&gt;-&amp;gt;{path}&lt;/span&gt;&lt;span class="p"&gt;";&lt;/span&gt;

        &lt;span class="c1"&gt;# Intercept the outgoing stream so we can observe the final status.&lt;/span&gt;
        &lt;span class="k"&gt;my&lt;/span&gt; &lt;span class="nv"&gt;$status&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
        &lt;span class="k"&gt;my&lt;/span&gt; &lt;span class="nv"&gt;$wrapped_send&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nv"&gt;async&lt;/span&gt; &lt;span class="k"&gt;sub &lt;/span&gt;&lt;span class="p"&gt;($event) {&lt;/span&gt;
            &lt;span class="nv"&gt;$status&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nv"&gt;$event&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="nv"&gt;status&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;
                &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="nv"&gt;$event&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="nv"&gt;type&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="ow"&gt;eq&lt;/span&gt; &lt;span class="p"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;http.response.start&lt;/span&gt;&lt;span class="p"&gt;';&lt;/span&gt;
            &lt;span class="nv"&gt;await&lt;/span&gt; &lt;span class="nv"&gt;$send&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nv"&gt;$event&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
        &lt;span class="p"&gt;};&lt;/span&gt;

        &lt;span class="nv"&gt;await&lt;/span&gt; &lt;span class="nv"&gt;$app&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nv"&gt;$scope&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nv"&gt;$receive&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nv"&gt;$wrapped_send&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

        &lt;span class="k"&gt;my&lt;/span&gt; &lt;span class="nv"&gt;$elapsed&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nb"&gt;time&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="o"&gt;-&lt;/span&gt; &lt;span class="nv"&gt;$start_time&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
        &lt;span class="nb"&gt;printf&lt;/span&gt; &lt;span class="p"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;[LOG] Outgoing Status: %d (Processed in %.4f seconds)&lt;/span&gt;&lt;span class="se"&gt;\n&lt;/span&gt;&lt;span class="p"&gt;",&lt;/span&gt;
            &lt;span class="nv"&gt;$status&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nv"&gt;$elapsed&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
    &lt;span class="p"&gt;};&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The PSGI original returns a &lt;code&gt;[$status, $headers, $body]&lt;/code&gt; arrayref it can read directly. In PAGI (as in ASGI) the response is &lt;em&gt;streamed&lt;/em&gt; as events, so the idiomatic move is to wrap &lt;code&gt;$send&lt;/code&gt; and observe &lt;code&gt;http.response.start&lt;/code&gt; — exactly what the bundled &lt;code&gt;PAGI::Middleware::Runtime&lt;/code&gt; and &lt;code&gt;PAGI::Middleware::AccessLog&lt;/code&gt; do.&lt;/p&gt;




&lt;h2&gt;
  
  
  Authenticator middleware
&lt;/h2&gt;

&lt;p&gt;Reads &lt;code&gt;X-Auth-Token&lt;/code&gt;. On the magic value it logs, &lt;strong&gt;injects&lt;/strong&gt; &lt;code&gt;user_id =&amp;gt; 456&lt;/code&gt; for the layers below, and continues. Otherwise it logs and &lt;strong&gt;short-circuits&lt;/strong&gt; with a 401 — the inner app is never called.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight perl"&gt;&lt;code&gt;&lt;span class="nb"&gt;package&lt;/span&gt; &lt;span class="nn"&gt;MyApp::Middleware::&lt;/span&gt;&lt;span class="nv"&gt;Authenticator&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="k"&gt;use&lt;/span&gt; &lt;span class="nv"&gt;v5&lt;/span&gt;&lt;span class="mf"&gt;.40&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="k"&gt;use&lt;/span&gt; &lt;span class="nv"&gt;parent&lt;/span&gt; &lt;span class="p"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;PAGI::Middleware&lt;/span&gt;&lt;span class="p"&gt;';&lt;/span&gt;
&lt;span class="k"&gt;use&lt;/span&gt; &lt;span class="nn"&gt;Future::&lt;/span&gt;&lt;span class="nv"&gt;AsyncAwait&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="k"&gt;use&lt;/span&gt; &lt;span class="nn"&gt;PAGI::&lt;/span&gt;&lt;span class="nv"&gt;Request&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="k"&gt;use&lt;/span&gt; &lt;span class="nn"&gt;PAGI::&lt;/span&gt;&lt;span class="nv"&gt;Response&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="k"&gt;sub &lt;/span&gt;&lt;span class="nf"&gt;wrap&lt;/span&gt; &lt;span class="p"&gt;($self, $app) {&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nv"&gt;async&lt;/span&gt; &lt;span class="k"&gt;sub &lt;/span&gt;&lt;span class="p"&gt;($scope, $receive, $send) {&lt;/span&gt;

        &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nv"&gt;await&lt;/span&gt; &lt;span class="nv"&gt;$app&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nv"&gt;$scope&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nv"&gt;$receive&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nv"&gt;$send&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
          &lt;span class="k"&gt;unless&lt;/span&gt; &lt;span class="nv"&gt;$scope&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="nv"&gt;type&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="ow"&gt;eq&lt;/span&gt; &lt;span class="p"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;http&lt;/span&gt;&lt;span class="p"&gt;';&lt;/span&gt;

        &lt;span class="k"&gt;my&lt;/span&gt; &lt;span class="nv"&gt;$req&lt;/span&gt;   &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nn"&gt;PAGI::&lt;/span&gt;&lt;span class="nv"&gt;Request&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="k"&gt;new&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nv"&gt;$scope&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nv"&gt;$receive&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
        &lt;span class="k"&gt;my&lt;/span&gt; &lt;span class="nv"&gt;$token&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nv"&gt;$req&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="nv"&gt;header&lt;/span&gt;&lt;span class="p"&gt;('&lt;/span&gt;&lt;span class="s1"&gt;X-Auth-Token&lt;/span&gt;&lt;span class="p"&gt;');&lt;/span&gt;

        &lt;span class="k"&gt;unless&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nb"&gt;defined&lt;/span&gt; &lt;span class="nv"&gt;$token&lt;/span&gt; &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt; &lt;span class="nv"&gt;$token&lt;/span&gt; &lt;span class="ow"&gt;eq&lt;/span&gt; &lt;span class="p"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;secret-password-123&lt;/span&gt;&lt;span class="p"&gt;')&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
            &lt;span class="nv"&gt;say&lt;/span&gt; &lt;span class="p"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;[AUTH] Access Denied. Short-circuiting.&lt;/span&gt;&lt;span class="p"&gt;";&lt;/span&gt;

            &lt;span class="c1"&gt;# Short-circuit: never call $app. A response is just a value we&lt;/span&gt;
            &lt;span class="c1"&gt;# render onto the connection ourselves.&lt;/span&gt;
            &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nv"&gt;await&lt;/span&gt; &lt;span class="nn"&gt;PAGI::&lt;/span&gt;&lt;span class="nv"&gt;Response&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="nv"&gt;text&lt;/span&gt;&lt;span class="p"&gt;('&lt;/span&gt;&lt;span class="s1"&gt;Unauthorized&lt;/span&gt;&lt;span class="p"&gt;',&lt;/span&gt; &lt;span class="s"&gt;status&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="mi"&gt;401&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
                &lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="nv"&gt;respond&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nv"&gt;$send&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
        &lt;span class="p"&gt;}&lt;/span&gt;

        &lt;span class="nv"&gt;say&lt;/span&gt; &lt;span class="p"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;[AUTH] Valid token. Granting access to User #456.&lt;/span&gt;&lt;span class="p"&gt;";&lt;/span&gt;

        &lt;span class="c1"&gt;# Inject per-request context for downstream layers. modify_scope&lt;/span&gt;
        &lt;span class="c1"&gt;# shallow-copies the scope so the addition is only visible to the&lt;/span&gt;
        &lt;span class="c1"&gt;# inner app, never leaking back out to middleware above us.&lt;/span&gt;
        &lt;span class="k"&gt;my&lt;/span&gt; &lt;span class="nv"&gt;$authed_scope&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nv"&gt;$self&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="nv"&gt;modify_scope&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nv"&gt;$scope&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="s"&gt;user_id&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="mi"&gt;456&lt;/span&gt; &lt;span class="p"&gt;});&lt;/span&gt;

        &lt;span class="nv"&gt;await&lt;/span&gt; &lt;span class="nv"&gt;$app&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nv"&gt;$authed_scope&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nv"&gt;$receive&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nv"&gt;$send&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
    &lt;span class="p"&gt;};&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;code&gt;PAGI::Request&lt;/code&gt; is a convenience wrapper over the raw scope — &lt;code&gt;$req-&amp;gt;header('X-Auth-Token')&lt;/code&gt; is case-insensitive, just like &lt;code&gt;$req-&amp;gt;header(...)&lt;/code&gt; in the Plack version. (For a real app, &lt;code&gt;PAGI::Middleware::Auth::Bearer&lt;/code&gt; ships this pattern as a configurable building block.)&lt;/p&gt;




&lt;h2&gt;
  
  
  ProfileRouter middleware
&lt;/h2&gt;

&lt;p&gt;If the path is &lt;code&gt;/api/profile&lt;/code&gt;, it answers directly with JSON — reading the &lt;code&gt;user_id&lt;/code&gt; the Authenticator injected, defaulting to &lt;code&gt;'Guest'&lt;/code&gt; if it somehow ran without auth. Any other path falls through to whatever is below.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight perl"&gt;&lt;code&gt;&lt;span class="nb"&gt;package&lt;/span&gt; &lt;span class="nn"&gt;MyApp::Middleware::&lt;/span&gt;&lt;span class="nv"&gt;ProfileRouter&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="k"&gt;use&lt;/span&gt; &lt;span class="nv"&gt;v5&lt;/span&gt;&lt;span class="mf"&gt;.40&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="k"&gt;use&lt;/span&gt; &lt;span class="nv"&gt;parent&lt;/span&gt; &lt;span class="p"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;PAGI::Middleware&lt;/span&gt;&lt;span class="p"&gt;';&lt;/span&gt;
&lt;span class="k"&gt;use&lt;/span&gt; &lt;span class="nn"&gt;Future::&lt;/span&gt;&lt;span class="nv"&gt;AsyncAwait&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="k"&gt;use&lt;/span&gt; &lt;span class="nn"&gt;PAGI::&lt;/span&gt;&lt;span class="nv"&gt;Response&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="k"&gt;sub &lt;/span&gt;&lt;span class="nf"&gt;wrap&lt;/span&gt; &lt;span class="p"&gt;($self, $app) {&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nv"&gt;async&lt;/span&gt; &lt;span class="k"&gt;sub &lt;/span&gt;&lt;span class="p"&gt;($scope, $receive, $send) {&lt;/span&gt;

        &lt;span class="c1"&gt;# Not our route — hand off to the inner app.&lt;/span&gt;
        &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nv"&gt;await&lt;/span&gt; &lt;span class="nv"&gt;$app&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nv"&gt;$scope&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nv"&gt;$receive&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nv"&gt;$send&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
          &lt;span class="k"&gt;unless&lt;/span&gt; &lt;span class="nv"&gt;$scope&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="nv"&gt;type&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="ow"&gt;eq&lt;/span&gt; &lt;span class="p"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;http&lt;/span&gt;&lt;span class="p"&gt;'&lt;/span&gt; &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt; &lt;span class="nv"&gt;$scope&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="nv"&gt;path&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="ow"&gt;eq&lt;/span&gt; &lt;span class="p"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;/api/profile&lt;/span&gt;&lt;span class="p"&gt;';&lt;/span&gt;

        &lt;span class="c1"&gt;# Read the context the Authenticator injected upstream.&lt;/span&gt;
        &lt;span class="nv"&gt;say&lt;/span&gt; &lt;span class="p"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;[ROUTER] Handling /api/profile directly inside middleware.&lt;/span&gt;&lt;span class="p"&gt;";&lt;/span&gt;

        &lt;span class="k"&gt;my&lt;/span&gt; &lt;span class="nv"&gt;$data&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
            &lt;span class="s"&gt;user_id&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="nv"&gt;$scope&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="nv"&gt;user_id&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="sr"&gt;//&lt;/span&gt; &lt;span class="p"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;Guest&lt;/span&gt;&lt;span class="p"&gt;',&lt;/span&gt;
            &lt;span class="s"&gt;name&lt;/span&gt;    &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;Alice Perl&lt;/span&gt;&lt;span class="p"&gt;',&lt;/span&gt;
            &lt;span class="s"&gt;status&lt;/span&gt;  &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;Fully Delegated Architecture&lt;/span&gt;&lt;span class="p"&gt;',&lt;/span&gt;
        &lt;span class="p"&gt;};&lt;/span&gt;

        &lt;span class="nv"&gt;await&lt;/span&gt; &lt;span class="nn"&gt;PAGI::&lt;/span&gt;&lt;span class="nv"&gt;Response&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="nv"&gt;json&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nv"&gt;$data&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="nv"&gt;respond&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nv"&gt;$send&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
    &lt;span class="p"&gt;};&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;code&gt;$scope-&amp;gt;{user_id}&lt;/code&gt; here is the PAGI counterpart to &lt;code&gt;$env-&amp;gt;{'custom.user_id'}&lt;/code&gt; (PSGI) and &lt;code&gt;request.state.user_id&lt;/code&gt; (Starlette). It is present because &lt;code&gt;modify_scope&lt;/code&gt; put it on the copy that flowed down from the Authenticator.&lt;/p&gt;




&lt;h2&gt;
  
  
  Assembling the app
&lt;/h2&gt;

&lt;p&gt;PAGI ships a &lt;code&gt;Plack::Builder&lt;/code&gt;-style DSL in &lt;code&gt;PAGI::Middleware::Builder&lt;/code&gt;. The shape mirrors the original &lt;code&gt;builder { enable ...; $fallback }&lt;/code&gt; almost line for line:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight perl"&gt;&lt;code&gt;&lt;span class="c1"&gt;#!/usr/bin/env perl&lt;/span&gt;

&lt;span class="k"&gt;use&lt;/span&gt; &lt;span class="nv"&gt;v5&lt;/span&gt;&lt;span class="mf"&gt;.40&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="k"&gt;use&lt;/span&gt; &lt;span class="nv"&gt;lib&lt;/span&gt; &lt;span class="p"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;lib&lt;/span&gt;&lt;span class="p"&gt;';&lt;/span&gt;

&lt;span class="k"&gt;use&lt;/span&gt; &lt;span class="nn"&gt;PAGI::Middleware::&lt;/span&gt;&lt;span class="nv"&gt;Builder&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="k"&gt;use&lt;/span&gt; &lt;span class="nn"&gt;PAGI::&lt;/span&gt;&lt;span class="nv"&gt;Response&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="c1"&gt;# Compose the stack, outermost first. Logger wraps Authenticator wraps&lt;/span&gt;
&lt;span class="c1"&gt;# ProfileRouter wraps the 404 fallback. The '^' prefix means "this is a&lt;/span&gt;
&lt;span class="c1"&gt;# fully-qualified class name", the PAGI equivalent of Plack's '+'.&lt;/span&gt;
&lt;span class="c1"&gt;#&lt;/span&gt;
&lt;span class="c1"&gt;# The fallback is just a Response value: the builder coerces its final&lt;/span&gt;
&lt;span class="c1"&gt;# expression through PAGI::Utils::to_app, and a PAGI::Response knows how to&lt;/span&gt;
&lt;span class="c1"&gt;# turn itself into an app. It fires only when no middleware short-circuited.&lt;/span&gt;
&lt;span class="nv"&gt;builder&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="nv"&gt;enable&lt;/span&gt; &lt;span class="p"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;^MyApp::Middleware::Logger&lt;/span&gt;&lt;span class="p"&gt;';&lt;/span&gt;
    &lt;span class="nv"&gt;enable&lt;/span&gt; &lt;span class="p"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;^MyApp::Middleware::Authenticator&lt;/span&gt;&lt;span class="p"&gt;';&lt;/span&gt;
    &lt;span class="nv"&gt;enable&lt;/span&gt; &lt;span class="p"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;^MyApp::Middleware::ProfileRouter&lt;/span&gt;&lt;span class="p"&gt;';&lt;/span&gt;
    &lt;span class="nn"&gt;PAGI::&lt;/span&gt;&lt;span class="nv"&gt;Response&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="nv"&gt;text&lt;/span&gt;&lt;span class="p"&gt;('&lt;/span&gt;&lt;span class="s1"&gt;Resource Not Found&lt;/span&gt;&lt;span class="p"&gt;',&lt;/span&gt; &lt;span class="s"&gt;status&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="mi"&gt;404&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="p"&gt;};&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Because a response &lt;em&gt;is&lt;/em&gt; a value in PAGI, the fallback needs no wrapper coderef — the builder accepts the &lt;code&gt;PAGI::Response&lt;/code&gt; object directly and calls its &lt;code&gt;to_app&lt;/code&gt; for you. (The original PSGI version needs the explicit &lt;code&gt;sub { [404, …] }&lt;/code&gt; because PSGI's inner app must be a callable.)&lt;/p&gt;

&lt;p&gt;The one syntactic difference worth flagging: where Plack writes &lt;code&gt;enable '+MyApp::Middleware::Logger'&lt;/code&gt; to mean "don't prepend the framework namespace," PAGI writes &lt;code&gt;enable '^MyApp::Middleware::Logger'&lt;/code&gt;. Bare names like &lt;code&gt;enable 'Runtime'&lt;/code&gt; are resolved to &lt;code&gt;PAGI::Middleware::Runtime&lt;/code&gt;; the &lt;code&gt;^&lt;/code&gt; opts out of that prefixing.&lt;/p&gt;

&lt;p&gt;Middleware runs outermost-first, so the request flows &lt;strong&gt;Logger → Authenticator → ProfileRouter → fallback&lt;/strong&gt; and the response unwinds back out through the same layers — which is why the Logger sees the final status of whatever any inner layer produced.&lt;/p&gt;




&lt;h2&gt;
  
  
  Running it
&lt;/h2&gt;

&lt;p&gt;The example is exercised in-process with &lt;code&gt;PAGI::Test::Client&lt;/code&gt;, which constructs the &lt;code&gt;($scope, $receive, $send)&lt;/code&gt; messages and invokes the app directly — no socket required.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight perl"&gt;&lt;code&gt;&lt;span class="k"&gt;use&lt;/span&gt; &lt;span class="nv"&gt;v5&lt;/span&gt;&lt;span class="mf"&gt;.40&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="k"&gt;use&lt;/span&gt; &lt;span class="nv"&gt;lib&lt;/span&gt; &lt;span class="p"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;lib&lt;/span&gt;&lt;span class="p"&gt;';&lt;/span&gt;

&lt;span class="k"&gt;use&lt;/span&gt; &lt;span class="nn"&gt;Test2::&lt;/span&gt;&lt;span class="nv"&gt;V0&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="k"&gt;use&lt;/span&gt; &lt;span class="nn"&gt;JSON::&lt;/span&gt;&lt;span class="nv"&gt;MaybeXS&lt;/span&gt; &lt;span class="sx"&gt;qw(decode_json)&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="k"&gt;use&lt;/span&gt; &lt;span class="nn"&gt;PAGI::Test::&lt;/span&gt;&lt;span class="nv"&gt;Client&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="k"&gt;my&lt;/span&gt; &lt;span class="nv"&gt;$app&lt;/span&gt;    &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;do&lt;/span&gt; &lt;span class="p"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;./app.pl&lt;/span&gt;&lt;span class="p"&gt;';&lt;/span&gt;
&lt;span class="k"&gt;my&lt;/span&gt; &lt;span class="nv"&gt;$client&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nn"&gt;PAGI::Test::&lt;/span&gt;&lt;span class="nv"&gt;Client&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="k"&gt;new&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;app&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="nv"&gt;$app&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

&lt;span class="nv"&gt;subtest&lt;/span&gt; &lt;span class="p"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;no token: Authenticator short-circuits with 401&lt;/span&gt;&lt;span class="p"&gt;'&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="k"&gt;sub &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;my&lt;/span&gt; &lt;span class="nv"&gt;$res&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nv"&gt;$client&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="nv"&gt;get&lt;/span&gt;&lt;span class="p"&gt;('&lt;/span&gt;&lt;span class="s1"&gt;/api/profile&lt;/span&gt;&lt;span class="p"&gt;');&lt;/span&gt;

    &lt;span class="nv"&gt;is&lt;/span&gt; &lt;span class="nv"&gt;$res&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="nv"&gt;status&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;401&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;            &lt;span class="p"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;status is 401&lt;/span&gt;&lt;span class="p"&gt;';&lt;/span&gt;
    &lt;span class="nv"&gt;is&lt;/span&gt; &lt;span class="nv"&gt;$res&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="nv"&gt;text&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;   &lt;span class="p"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;Unauthorized&lt;/span&gt;&lt;span class="p"&gt;',&lt;/span&gt; &lt;span class="p"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;body is Unauthorized&lt;/span&gt;&lt;span class="p"&gt;';&lt;/span&gt;
&lt;span class="p"&gt;};&lt;/span&gt;

&lt;span class="nv"&gt;subtest&lt;/span&gt; &lt;span class="p"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;valid token + /api/profile: ProfileRouter answers with JSON&lt;/span&gt;&lt;span class="p"&gt;'&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="k"&gt;sub &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;my&lt;/span&gt; &lt;span class="nv"&gt;$res&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nv"&gt;$client&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="nv"&gt;get&lt;/span&gt;&lt;span class="p"&gt;('&lt;/span&gt;&lt;span class="s1"&gt;/api/profile&lt;/span&gt;&lt;span class="p"&gt;',&lt;/span&gt;
        &lt;span class="s"&gt;headers&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="p"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;X-Auth-Token&lt;/span&gt;&lt;span class="p"&gt;'&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;secret-password-123&lt;/span&gt;&lt;span class="p"&gt;'&lt;/span&gt; &lt;span class="p"&gt;},&lt;/span&gt;
    &lt;span class="p"&gt;);&lt;/span&gt;

    &lt;span class="nv"&gt;is&lt;/span&gt; &lt;span class="nv"&gt;$res&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="nv"&gt;status&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;200&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;status is 200&lt;/span&gt;&lt;span class="p"&gt;';&lt;/span&gt;
    &lt;span class="nv"&gt;is&lt;/span&gt; &lt;span class="nv"&gt;decode_json&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nv"&gt;$res&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="nv"&gt;text&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
        &lt;span class="p"&gt;{&lt;/span&gt;
            &lt;span class="s"&gt;user_id&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="mi"&gt;456&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
            &lt;span class="s"&gt;name&lt;/span&gt;    &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;Alice Perl&lt;/span&gt;&lt;span class="p"&gt;',&lt;/span&gt;
            &lt;span class="s"&gt;status&lt;/span&gt;  &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;Fully Delegated Architecture&lt;/span&gt;&lt;span class="p"&gt;',&lt;/span&gt;
        &lt;span class="p"&gt;},&lt;/span&gt;
        &lt;span class="p"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;profile payload, with user_id injected by the Authenticator&lt;/span&gt;&lt;span class="p"&gt;';&lt;/span&gt;
&lt;span class="p"&gt;};&lt;/span&gt;

&lt;span class="nv"&gt;subtest&lt;/span&gt; &lt;span class="p"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;valid token + unknown path: falls through to 404&lt;/span&gt;&lt;span class="p"&gt;'&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="k"&gt;sub &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;my&lt;/span&gt; &lt;span class="nv"&gt;$res&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nv"&gt;$client&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="nv"&gt;get&lt;/span&gt;&lt;span class="p"&gt;('&lt;/span&gt;&lt;span class="s1"&gt;/somewhere/else&lt;/span&gt;&lt;span class="p"&gt;',&lt;/span&gt;
        &lt;span class="s"&gt;headers&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="p"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;X-Auth-Token&lt;/span&gt;&lt;span class="p"&gt;'&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;secret-password-123&lt;/span&gt;&lt;span class="p"&gt;'&lt;/span&gt; &lt;span class="p"&gt;},&lt;/span&gt;
    &lt;span class="p"&gt;);&lt;/span&gt;

    &lt;span class="nv"&gt;is&lt;/span&gt; &lt;span class="nv"&gt;$res&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="nv"&gt;status&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;404&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;                  &lt;span class="p"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;status is 404&lt;/span&gt;&lt;span class="p"&gt;';&lt;/span&gt;
    &lt;span class="nv"&gt;is&lt;/span&gt; &lt;span class="nv"&gt;$res&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="nv"&gt;text&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;   &lt;span class="p"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;Resource Not Found&lt;/span&gt;&lt;span class="p"&gt;',&lt;/span&gt; &lt;span class="p"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;fallback body&lt;/span&gt;&lt;span class="p"&gt;';&lt;/span&gt;
&lt;span class="p"&gt;};&lt;/span&gt;

&lt;span class="nv"&gt;done_testing&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;code&gt;Test2::V0&lt;/code&gt;'s &lt;code&gt;is&lt;/code&gt; does a deep, structural comparison, so the whole profile payload is checked in one assertion instead of field by field.&lt;/p&gt;

&lt;p&gt;Actual output (&lt;code&gt;prove -v&lt;/code&gt;), with the middleware's own &lt;code&gt;[LOG]/[AUTH]/[ROUTER]&lt;/code&gt; lines interleaved:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;[LOG] Incoming: GET /api/profile
[AUTH] Access Denied. Short-circuiting.
[LOG] Outgoing Status: 401 (Processed in 0.0001 seconds)
ok 1 - no token: Authenticator short-circuits with 401 {
    ok 1 - status is 401
    ok 2 - body is Unauthorized
    1..2
}
[LOG] Incoming: GET /api/profile
[AUTH] Valid token. Granting access to User #456.
[ROUTER] Handling /api/profile directly inside middleware.
[LOG] Outgoing Status: 200 (Processed in 0.0002 seconds)
ok 2 - valid token + /api/profile: ProfileRouter answers with JSON {
    ok 1 - status is 200
    ok 2 - profile payload, with user_id injected by the Authenticator
    1..2
}
[LOG] Incoming: GET /somewhere/else
[AUTH] Valid token. Granting access to User #456.
[LOG] Outgoing Status: 404 (Processed in 0.0001 seconds)
ok 3 - valid token + unknown path: falls through to 404 {
    ok 1 - status is 404
    ok 2 - fallback body
    1..2
}
1..3
ok
All tests successful.
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The JSON body for the authorized profile request is, verbatim:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight json"&gt;&lt;code&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="nl"&gt;"name"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="s2"&gt;"Alice Perl"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="nl"&gt;"status"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="s2"&gt;"Fully Delegated Architecture"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="nl"&gt;"user_id"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="mi"&gt;456&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;(&lt;code&gt;PAGI::Response-&amp;gt;json&lt;/code&gt; encodes with sorted keys, which is why they come out alphabetical.)&lt;/p&gt;

&lt;p&gt;To run it against a real server instead of the test client, the same &lt;code&gt;app.pl&lt;/code&gt; is a complete PAGI application — point a PAGI server at it:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;pagi-server &lt;span class="nt"&gt;--app&lt;/span&gt; app.pl &lt;span class="nt"&gt;--port&lt;/span&gt; 5000

curl http://localhost:5000/api/profile                                  &lt;span class="c"&gt;# 401&lt;/span&gt;
curl &lt;span class="nt"&gt;-H&lt;/span&gt; &lt;span class="s1"&gt;'X-Auth-Token: secret-password-123'&lt;/span&gt; http://localhost:5000/api/profile   &lt;span class="c"&gt;# JSON&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;






&lt;h2&gt;
  
  
  How it lines up with the original
&lt;/h2&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Concern&lt;/th&gt;
&lt;th&gt;Plack / PSGI&lt;/th&gt;
&lt;th&gt;Starlette / ASGI&lt;/th&gt;
&lt;th&gt;PAGI&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Middleware unit&lt;/td&gt;
&lt;td&gt;
&lt;code&gt;Plack::Middleware&lt;/code&gt; + &lt;code&gt;call($env)&lt;/code&gt;
&lt;/td&gt;
&lt;td&gt;
&lt;code&gt;BaseHTTPMiddleware&lt;/code&gt; + &lt;code&gt;dispatch(request, call_next)&lt;/code&gt;
&lt;/td&gt;
&lt;td&gt;
&lt;code&gt;PAGI::Middleware&lt;/code&gt; + &lt;code&gt;wrap($app)&lt;/code&gt; returning an async sub&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Inner app handle&lt;/td&gt;
&lt;td&gt;&lt;code&gt;$self-&amp;gt;app-&amp;gt;($env)&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;await call_next(request)&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;await $app-&amp;gt;($scope, $receive, $send)&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Read a header&lt;/td&gt;
&lt;td&gt;&lt;code&gt;$req-&amp;gt;header('X-Auth-Token')&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;request.headers.get(...)&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;$req-&amp;gt;header('X-Auth-Token')&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Inject context&lt;/td&gt;
&lt;td&gt;&lt;code&gt;$env-&amp;gt;{'custom.user_id'} = 456&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;request.state.user_id = 456&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;$self-&amp;gt;modify_scope($scope, { user_id =&amp;gt; 456 })&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Short-circuit&lt;/td&gt;
&lt;td&gt;return &lt;code&gt;[401, …]&lt;/code&gt;
&lt;/td&gt;
&lt;td&gt;return &lt;code&gt;PlainTextResponse(...)&lt;/code&gt;
&lt;/td&gt;
&lt;td&gt;build a &lt;code&gt;PAGI::Response&lt;/code&gt;, &lt;code&gt;-&amp;gt;respond($send)&lt;/code&gt;, skip &lt;code&gt;$app&lt;/code&gt;
&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Compose stack&lt;/td&gt;
&lt;td&gt;&lt;code&gt;builder { enable '+...'; $fallback }&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;Starlette(middleware =&amp;gt; [...])&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;builder { enable '^...'; $fallback }&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;The structure ports essentially one-to-one. The only conceptual shift from PSGI is the one the Python version already makes: responses are &lt;strong&gt;streamed events&lt;/strong&gt; rather than a returned tuple, so "look at the response" means wrapping &lt;code&gt;$send&lt;/code&gt;, and "context" lives on a copied scope rather than a mutated environment.&lt;/p&gt;

</description>
      <category>perl</category>
      <category>webdev</category>
      <category>pagi</category>
    </item>
    <item>
      <title>PAGI Distribution Split</title>
      <dc:creator>John Napiorkowski</dc:creator>
      <pubDate>Sat, 27 Jun 2026 01:26:47 +0000</pubDate>
      <link>https://dev.to/jjn1056/pagi-distribution-split-1kdo</link>
      <guid>https://dev.to/jjn1056/pagi-distribution-split-1kdo</guid>
      <description>&lt;p&gt;The news: PAGI is now &lt;strong&gt;three CPAN distributions&lt;/strong&gt; instead of one.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;&lt;a href="https://metacpan.org/dist/PAGI-Server" rel="noopener noreferrer"&gt;PAGI-Server&lt;/a&gt;&lt;/strong&gt; — the reference server&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;&lt;a href="https://metacpan.org/dist/PAGI-Tools" rel="noopener noreferrer"&gt;PAGI-Tools&lt;/a&gt;&lt;/strong&gt; — the application toolkit&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;&lt;a href="https://metacpan.org/dist/PAGI" rel="noopener noreferrer"&gt;PAGI&lt;/a&gt;&lt;/strong&gt; — the specification&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Install what you actually run
&lt;/h2&gt;

&lt;p&gt;In practice, almost everyone starts the same way: &lt;code&gt;cpanm PAGI::Server&lt;/code&gt; to get a&lt;br&gt;
server that runs PAGI apps, and probably &lt;code&gt;PAGI::Tools&lt;/code&gt; for the request/response&lt;br&gt;
helpers, router, and middleware you'll want while building one. That's the&lt;br&gt;
common case, and the split is built around it — &lt;strong&gt;you install the pieces you&lt;br&gt;
use&lt;/strong&gt; instead of swallowing one monolith that bundled the server, the toolkit,&lt;br&gt;
and the spec together.&lt;/p&gt;

&lt;p&gt;Underneath both sits the &lt;strong&gt;specification&lt;/strong&gt;: a small, deliberately stable&lt;br&gt;
contract — the shape of &lt;code&gt;$scope&lt;/code&gt;, &lt;code&gt;$receive&lt;/code&gt;, &lt;code&gt;$send&lt;/code&gt;, and the event types —&lt;br&gt;
that the server and the toolkit both implement. Keeping it in its own&lt;br&gt;
distribution is the real reason to split: the fast-moving parts (the server and&lt;br&gt;
the toolkit, where the churn actually lives) can iterate freely without&lt;br&gt;
destabilizing the protocol you write your apps against.&lt;/p&gt;

&lt;p&gt;To be candid: today there is exactly &lt;strong&gt;one&lt;/strong&gt; reference server, so "just depend&lt;br&gt;
on the bare spec" isn't something most people will do yet. That separation is&lt;br&gt;
forward-looking — it's what makes an &lt;em&gt;alternative&lt;/em&gt; server, or a framework built&lt;br&gt;
straight on the protocol, possible without forking everything else. The split&lt;br&gt;
lays that groundwork; it isn't pretending it's already the common path.&lt;/p&gt;

&lt;p&gt;And nothing breaks in the meantime: installing &lt;code&gt;PAGI&lt;/code&gt; still pulls in the server&lt;br&gt;
and toolkit as dependencies during the transition, so &lt;code&gt;cpanm PAGI&lt;/code&gt; gives you the&lt;br&gt;
whole stack exactly as before. That convenience dependency is temporary — depend&lt;br&gt;
on &lt;code&gt;PAGI::Server&lt;/code&gt; and/or &lt;code&gt;PAGI::Tools&lt;/code&gt; directly when you're ready.&lt;/p&gt;
&lt;h2&gt;
  
  
  What's notable in each
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;&lt;a href="https://metacpan.org/dist/PAGI-Server" rel="noopener noreferrer"&gt;PAGI-Server&lt;/a&gt; — the reference&lt;br&gt;
server.&lt;/strong&gt; An &lt;a href="https://metacpan.org/pod/IO::Async" rel="noopener noreferrer"&gt;IO::Async&lt;/a&gt;-based&lt;br&gt;
implementation handling HTTP/1.1, HTTP/2, WebSocket, SSE, TLS, and multi-worker&lt;br&gt;
pre-forking, with the &lt;code&gt;pagi-server&lt;/code&gt; CLI and a swappable-server runner. It's&lt;br&gt;
validated against a compliance suite, and any server implementing the&lt;br&gt;
documented contract is a drop-in alternative — which is exactly the door the&lt;br&gt;
split holds open.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;&lt;a href="https://metacpan.org/dist/PAGI-Tools" rel="noopener noreferrer"&gt;PAGI-Tools&lt;/a&gt; — the toolkit.&lt;/strong&gt; The&lt;br&gt;
ergonomics you reach for when actually building apps: a router and a&lt;br&gt;
class-based endpoint framework, a middleware suite, ready-made apps (static&lt;br&gt;
files, proxy, a PSGI bridge), and &lt;code&gt;Request&lt;/code&gt;/&lt;code&gt;Response&lt;/code&gt;/&lt;code&gt;Context&lt;/code&gt; helpers. A few&lt;br&gt;
highlights: &lt;code&gt;PAGI::Response&lt;/code&gt; is now a &lt;strong&gt;value&lt;/strong&gt; you build and then send (a clean&lt;br&gt;
split between assembling a response and committing it to the wire), a new&lt;br&gt;
ordered, case-insensitive &lt;strong&gt;&lt;code&gt;PAGI::Headers&lt;/code&gt;&lt;/strong&gt; container, and a &lt;code&gt;to_app&lt;/code&gt;&lt;br&gt;
coercion that lets every composition point accept coderefs, objects, or class&lt;br&gt;
names interchangeably. An in-process &lt;code&gt;PAGI::Test::Client&lt;/code&gt; lets you test apps&lt;br&gt;
without a live server.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;&lt;a href="https://metacpan.org/dist/PAGI" rel="noopener noreferrer"&gt;PAGI&lt;/a&gt; — the spec.&lt;/strong&gt; Now pure documentation&lt;br&gt;
(the module plus the &lt;code&gt;PAGI::Spec::*&lt;/code&gt; POD) and the canonical place to start: the&lt;br&gt;
&lt;a href="https://metacpan.org/pod/PAGI::Tutorial" rel="noopener noreferrer"&gt;tutorial&lt;/a&gt;, a cookbook, a&lt;br&gt;
&lt;a href="https://metacpan.org/pod/PAGI::PSGI" rel="noopener noreferrer"&gt;PSGI migration guide&lt;/a&gt;, and a&lt;br&gt;
&lt;a href="https://metacpan.org/pod/PAGI::Building" rel="noopener noreferrer"&gt;guide for framework authors&lt;/a&gt;. Recent&lt;br&gt;
protocol work made connection state observable — &lt;code&gt;pagi.connection&lt;/code&gt; gained&lt;br&gt;
&lt;strong&gt;&lt;code&gt;response_started&lt;/code&gt;&lt;/strong&gt; / &lt;code&gt;response_complete&lt;/code&gt; as observer-independent facts —&lt;br&gt;
pinned down scope shallow-clone semantics (how per-request state is shared vs.&lt;br&gt;
isolated through middleware), and now mandates server-side &lt;strong&gt;header&lt;br&gt;
byte-safety&lt;/strong&gt; (a server must reject CR/LF/NUL in header names and values rather&lt;br&gt;
than forwarding or silently rewriting them).&lt;/p&gt;
&lt;h2&gt;
  
  
  Status, and a note
&lt;/h2&gt;

&lt;p&gt;The &lt;strong&gt;specification is stable&lt;/strong&gt; — breaking changes won't be made except for&lt;br&gt;
critical security issues, so raw PAGI apps you write today keep working. The&lt;br&gt;
&lt;strong&gt;server and toolkit are beta&lt;/strong&gt;: solid, but not yet battle-tested in production,&lt;br&gt;
so run the server behind nginx/Apache/Caddy for now.&lt;/p&gt;

&lt;p&gt;PAGI is a labor of love for the future of async web programming in Perl. The&lt;br&gt;
project is dedicated to the memory of Matt S. Trout, who encouraged the&lt;br&gt;
author's first CPAN contribution two decades ago — without which none of this&lt;br&gt;
would exist.&lt;/p&gt;

&lt;p&gt;To kick the tires:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;cpanm PAGI::Server PAGI::Tools
pagi-server &lt;span class="nt"&gt;--app&lt;/span&gt; ./app.pl &lt;span class="nt"&gt;--port&lt;/span&gt; 5000
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Feedback, bug reports, and contributions are all welcome. Start with the&lt;br&gt;
&lt;a href="https://metacpan.org/pod/PAGI::Tutorial" rel="noopener noreferrer"&gt;tutorial&lt;/a&gt;; if you're coming from PSGI,&lt;br&gt;
the &lt;a href="https://metacpan.org/pod/PAGI::PSGI" rel="noopener noreferrer"&gt;migration guide&lt;/a&gt; maps the mental model&lt;br&gt;
across.&lt;/p&gt;

</description>
      <category>perl</category>
      <category>webdev</category>
      <category>pagi</category>
    </item>
    <item>
      <title>Perl PAGI Project Updates</title>
      <dc:creator>John Napiorkowski</dc:creator>
      <pubDate>Thu, 18 Jun 2026 15:31:36 +0000</pubDate>
      <link>https://dev.to/jjn1056/perl-pagi-project-updates-4602</link>
      <guid>https://dev.to/jjn1056/perl-pagi-project-updates-4602</guid>
      <description>&lt;p&gt;Quick update to anyone interested in upcoming changes to the PAGI project (spiritual successor to Plack/PSGI).&lt;/p&gt;

&lt;p&gt;1) Distribution split up: when we released PAGI, we initially released everything as one distribution.   PAGI (&lt;a href="https://metacpan.org/pod/PAGI" rel="noopener noreferrer"&gt;https://metacpan.org/pod/PAGI&lt;/a&gt;) currently has a) the PAGI specification; b) the reference server and c) a bunch of ease of use tools, similar to the role that the Plack distribution played for PSGI.  Putting everything into one place was just to make my life easier as in the early bunch of releases there was a lot of fixes and updates, most of which cut across all three parts of PAGI.   Also I wanted to make it easy for people getting into PAGI to be able to explore the ecosystem.   However now that code seems to be settling down having these in independent repos and releases makes more sense.  Going forward the PAGI repo will only update if the spec itself changes; PAGI::Server and PAGI::Tools (where all the utilities and helpers now go) likewise.   I think this will start to bring some stability to the ecosystem, especially now that PAGI::Server is functionally complete based on the goal chart I had for it initially.   So I will only update it to fix bugs and security issues.&lt;/p&gt;

&lt;p&gt;PAGI::Tools will probably continue to see evolution over the summer as I start to nail down more common use cases and identify patterns worth encapsulating.&lt;/p&gt;

&lt;p&gt;2) Specification clarifications and updates: The PAGI specification itself will move to v0.3 in the next release and it contains mostly clarifications and fixes.   Biggest change will be a more detailed mechanism for controlling streaming output, especially around handling back pressure as well as new callbacks to notice when the output buffer is getting full and when it clears.  Hopefully these changes will make it easier and more reliable to do streaming in PAGI.   PAGI::Server has been updated to match, and the response helper in PAGI::Tools has some updates around that as well.&lt;/p&gt;

&lt;p&gt;Currently all this sits on Github:&lt;/p&gt;

&lt;p&gt;&lt;a href="https://github.com/jjn1056/pagi" rel="noopener noreferrer"&gt;https://github.com/jjn1056/pagi&lt;/a&gt;&lt;br&gt;
&lt;a href="https://github.com/jjn1056/PAGI-Server" rel="noopener noreferrer"&gt;https://github.com/jjn1056/PAGI-Server&lt;/a&gt;&lt;br&gt;
&lt;a href="https://github.com/jjn1056/PAGI-Tools" rel="noopener noreferrer"&gt;https://github.com/jjn1056/PAGI-Tools&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Right now I'm giving the Thunderhorse author some time to vet a handful of changes needed to be compatible and I want to review all the updates and doc tweaks one last time.   This will land on CPAN before the Austin Perl Community conference first week of July (where I will be presenting on PAGI for those interested).&lt;br&gt;
For people who might currently be depending on the PAGI distribution, for the near term I will have PAGI::Server and PAGI::Tools as dependencies of PAGI, that way your currently toolchains don't break.  That will last a few months for transition.   &lt;/p&gt;

</description>
      <category>news</category>
      <category>opensource</category>
      <category>programming</category>
      <category>webdev</category>
    </item>
    <item>
      <title>PAGI::Server, now with HTTP/2!</title>
      <dc:creator>John Napiorkowski</dc:creator>
      <pubDate>Wed, 11 Feb 2026 19:05:39 +0000</pubDate>
      <link>https://dev.to/jjn1056/pagiserver-now-with-http2-37gf</link>
      <guid>https://dev.to/jjn1056/pagiserver-now-with-http2-37gf</guid>
      <description>&lt;p&gt;PAGI 0.001017 is on CPAN. The headline feature is HTTP/2 support in PAGI::Server, built on the nghttp2 C library and validated against h2spec's conformance suite. HTTP/2 is marked experimental in this release -- the protocol works, the compliance numbers are solid, and we want production feedback before dropping that label.&lt;/p&gt;

&lt;p&gt;This post focuses on why h2c (cleartext HTTP/2) between your reverse proxy and backend matters, and how PAGI::Server implements it.&lt;/p&gt;

&lt;h2&gt;
  
  
  The Quick Version
&lt;/h2&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="c"&gt;# Install the HTTP/2 dependency&lt;/span&gt;
cpanm Net::HTTP2::nghttp2

&lt;span class="c"&gt;# Run with HTTP/2 over TLS&lt;/span&gt;
pagi-server &lt;span class="nt"&gt;--http2&lt;/span&gt; &lt;span class="nt"&gt;--ssl-cert&lt;/span&gt; cert.pem &lt;span class="nt"&gt;--ssl-key&lt;/span&gt; key.pem app.pl

&lt;span class="c"&gt;# Run with cleartext HTTP/2 (h2c) -- behind a proxy&lt;/span&gt;
pagi-server &lt;span class="nt"&gt;--http2&lt;/span&gt; app.pl
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Your app code doesn't change. HTTP/2 is a transport concern handled entirely by the server. The same async handlers that serve HTTP/1.1 serve HTTP/2 without modification.&lt;/p&gt;

&lt;h2&gt;
  
  
  "I Have nginx in Front, Why Do I Care?"
&lt;/h2&gt;

&lt;p&gt;If nginx terminates TLS and speaks HTTP/2 to clients, why does the backend need HTTP/2 too?&lt;/p&gt;

&lt;p&gt;The answer is &lt;strong&gt;h2c&lt;/strong&gt; -- cleartext HTTP/2 between your proxy and your application server. No TLS overhead, but all of HTTP/2's protocol benefits on the internal hop: stream multiplexing over a single TCP connection, HPACK header compression (especially effective for repetitive internal headers like auth tokens and tracing IDs), and per-stream flow control so a slow response on one stream doesn't block others.&lt;/p&gt;

&lt;p&gt;The practical wins: fewer TCP connections between proxy and backend (one multiplexed h2c connection replaces a pool of HTTP/1.1 connections), less file descriptor and kernel memory pressure, and no TIME_WAIT churn from connection recycling.&lt;/p&gt;

&lt;h3&gt;
  
  
  Where h2c Matters
&lt;/h3&gt;

&lt;p&gt;&lt;strong&gt;gRPC&lt;/strong&gt; requires HTTP/2 -- it doesn't work over HTTP/1.1 at all. If you're building gRPC services, h2c is mandatory.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;API gateway fan-out&lt;/strong&gt; is where multiplexing shines. When your gateway fans out to 10 backend services per request, h2c means 1-2 connections per backend instead of a pool of 50-100.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Service mesh&lt;/strong&gt; environments (Envoy/Istio sidecars) default to HTTP/2 between services. A backend that speaks h2c natively means one less protocol translation.&lt;/p&gt;

&lt;h3&gt;
  
  
  A Note on Proxies
&lt;/h3&gt;

&lt;p&gt;Not all proxies handle h2c equally:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Envoy&lt;/strong&gt; has the best h2c upstream support with full multiplexing&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Caddy&lt;/strong&gt; makes it trivial: &lt;code&gt;reverse_proxy h2c://localhost:8080&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;nginx&lt;/strong&gt; supports h2c via &lt;code&gt;grpc_pass&lt;/code&gt; for gRPC workloads, but its generic &lt;code&gt;proxy_pass&lt;/code&gt; doesn't support &lt;code&gt;proxy_http_version 2.0&lt;/code&gt;
&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;For full multiplexing to backends, Envoy or Caddy are better choices than nginx today.&lt;/p&gt;

&lt;h2&gt;
  
  
  HTTP/2 Over TLS -- No Proxy Required
&lt;/h2&gt;

&lt;p&gt;h2c isn't the only mode. PAGI::Server also does full HTTP/2 over TLS with ALPN negotiation:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;pagi-server &lt;span class="nt"&gt;--http2&lt;/span&gt; &lt;span class="nt"&gt;--ssl-cert&lt;/span&gt; cert.pem &lt;span class="nt"&gt;--ssl-key&lt;/span&gt; key.pem app.pl
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This is useful when you don't want the overhead or complexity of a reverse proxy -- internal tools, admin dashboards, development servers, or any app where the traffic doesn't justify a separate proxy layer. Browsers get HTTP/2 directly, with TLS, no nginx required.&lt;/p&gt;

&lt;h2&gt;
  
  
  What PAGI::Server Does
&lt;/h2&gt;

&lt;h3&gt;
  
  
  Dual-Mode Protocol Detection
&lt;/h3&gt;

&lt;p&gt;With TLS, PAGI::Server uses ALPN negotiation during the handshake -- advertising &lt;code&gt;h2&lt;/code&gt; and &lt;code&gt;http/1.1&lt;/code&gt;, letting the client choose. The protocol is decided before the first byte of application data.&lt;/p&gt;

&lt;p&gt;Without TLS (h2c mode), PAGI::Server inspects the first 24 bytes of each connection for the HTTP/2 client connection preface. If it matches, the connection upgrades to HTTP/2. If not, it falls through to HTTP/1.1. Both protocols coexist on the same port, same worker -- no configuration needed beyond &lt;code&gt;--http2&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;Either way, HTTP/1.1 clients are still served normally. The server handles both protocols on the same port.&lt;/p&gt;

&lt;h3&gt;
  
  
  WebSocket over HTTP/2 (RFC 8441)
&lt;/h3&gt;

&lt;p&gt;Most HTTP/2 implementations skip this. PAGI::Server supports the Extended CONNECT protocol from RFC 8441, which tunnels WebSocket connections over HTTP/2 streams. Multiple WebSocket connections multiplex over a single TCP connection instead of requiring one TCP connection each.&lt;/p&gt;

&lt;h3&gt;
  
  
  Compliance
&lt;/h3&gt;

&lt;p&gt;Built on nghttp2 (the same C library behind curl, Firefox, and Apache's mod_http2). PAGI::Server passes 137 of 146 h2spec conformance tests (93.8%). The 9 remaining failures are in nghttp2 itself and shared with every server that uses it. Load tested with h2load at 60,000 requests across 50 concurrent connections with no data loss or protocol violations.&lt;/p&gt;

&lt;p&gt;Full test-by-test results are published: &lt;a href="https://metacpan.org/release/JJNAPIORK/PAGI-0.001017/view/lib/PAGI/Server/Compliance.pod" rel="noopener noreferrer"&gt;HTTP/2 Compliance Results&lt;/a&gt;.&lt;/p&gt;

&lt;h3&gt;
  
  
  Multi-Worker and Tunable
&lt;/h3&gt;

&lt;p&gt;HTTP/2 works in multi-worker prefork mode. Each worker independently handles HTTP/2 sessions:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;pagi-server &lt;span class="nt"&gt;--http2&lt;/span&gt; &lt;span class="nt"&gt;--workers&lt;/span&gt; 4 app.pl
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Protocol settings are exposed for environments that need fine-tuning:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight perl"&gt;&lt;code&gt;&lt;span class="k"&gt;my&lt;/span&gt; &lt;span class="nv"&gt;$server&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nn"&gt;PAGI::&lt;/span&gt;&lt;span class="nv"&gt;Server&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="k"&gt;new&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="s"&gt;app&lt;/span&gt;   &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="nv"&gt;$app&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="s"&gt;http2&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="s"&gt;h2_max_concurrent_streams&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="mi"&gt;50&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;   &lt;span class="c1"&gt;# default: 100&lt;/span&gt;
    &lt;span class="s"&gt;h2_initial_window_size&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="mi"&gt;131072&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;  &lt;span class="c1"&gt;# default: 65535&lt;/span&gt;
    &lt;span class="s"&gt;h2_max_frame_size&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="mi"&gt;32768&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;        &lt;span class="c1"&gt;# default: 16384&lt;/span&gt;
    &lt;span class="s"&gt;h2_max_header_list_size&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="mi"&gt;32768&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;  &lt;span class="c1"&gt;# default: 65536&lt;/span&gt;
&lt;span class="p"&gt;);&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Most deployments won't need to touch these. The defaults follow the RFC recommendations.&lt;/p&gt;

&lt;h3&gt;
  
  
  Context in the Perl Ecosystem
&lt;/h3&gt;

&lt;p&gt;Perl has had HTTP/2 libraries on CPAN (Protocol::HTTP2, Net::HTTP2), but application servers haven't integrated them with validated compliance testing. PAGI::Server is the first to publish h2spec results and ship h2c with automatic protocol detection alongside HTTP/1.1. If you're currently running Starman, Twiggy, or Hypnotoad, none of them offer HTTP/2.&lt;/p&gt;

&lt;h2&gt;
  
  
  What Else Is in 0.001017
&lt;/h2&gt;

&lt;p&gt;The rest of the release is operational improvements:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Worker heartbeat monitoring&lt;/strong&gt; -- parent process detects workers with blocked event loops and replaces them via SIGKILL + respawn. Default 50s timeout. Only triggers on true event loop starvation; async handlers using &lt;code&gt;await&lt;/code&gt; are unaffected.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Custom access log format&lt;/strong&gt; -- format strings with atoms like &lt;code&gt;%a&lt;/code&gt; (address), &lt;code&gt;%s&lt;/code&gt; (status), &lt;code&gt;%D&lt;/code&gt; (duration).&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;TLS performance fix&lt;/strong&gt; -- shared SSL context via &lt;code&gt;SSL_reuse_ctx&lt;/code&gt; eliminates per-connection CA bundle parsing. 26x throughput improvement at 8+ concurrent TLS connections.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;SSE wire format fix&lt;/strong&gt; -- now handles CRLF, LF, and bare CR line endings per the SSE specification.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Multi-worker fixes&lt;/strong&gt; -- shutdown escalation, parameter pass-through, and various stability improvements.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Getting Started
&lt;/h2&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="c"&gt;# Install PAGI&lt;/span&gt;
cpanm PAGI

&lt;span class="c"&gt;# Install HTTP/2 support (optional)&lt;/span&gt;
cpanm Net::HTTP2::nghttp2

&lt;span class="c"&gt;# Run your app with HTTP/2&lt;/span&gt;
pagi-server &lt;span class="nt"&gt;--http2&lt;/span&gt; app.pl
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  Links
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;&lt;a href="https://metacpan.org/release/JJNAPIORK/PAGI-0.001017" rel="noopener noreferrer"&gt;PAGI 0.001017 on CPAN&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://metacpan.org/release/JJNAPIORK/PAGI-0.001017/view/lib/PAGI/Server/Compliance.pod" rel="noopener noreferrer"&gt;HTTP/2 Compliance Results&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://github.com/jjn1056/pagi" rel="noopener noreferrer"&gt;PAGI on GitHub&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;
&lt;a href="https://github.com/summerwind/h2spec" rel="noopener noreferrer"&gt;h2spec&lt;/a&gt; -- HTTP/2 conformance testing tool&lt;/li&gt;
&lt;/ul&gt;

</description>
      <category>perl</category>
      <category>webdev</category>
      <category>pagi</category>
    </item>
    <item>
      <title>PAGI 0.001016: Navigating the Future::IO Configuration Problem</title>
      <dc:creator>John Napiorkowski</dc:creator>
      <pubDate>Mon, 02 Feb 2026 18:34:13 +0000</pubDate>
      <link>https://dev.to/jjn1056/pagi-0001016-navigating-the-futureio-configuration-problem-451d</link>
      <guid>https://dev.to/jjn1056/pagi-0001016-navigating-the-futureio-configuration-problem-451d</guid>
      <description>&lt;p&gt;PAGI 0.001016 includes a decision we wrestled with for a while: where should Future::IO configuration live? This post walks through the tradeoffs we considered and why we landed where we did.&lt;/p&gt;

&lt;h2&gt;
  
  
  What is Future::IO?
&lt;/h2&gt;

&lt;p&gt;Perl's async ecosystem has multiple event loops: &lt;a href="https://metacpan.org/pod/IO::Async" rel="noopener noreferrer"&gt;IO::Async&lt;/a&gt;, &lt;a href="https://metacpan.org/pod/Mojo::IOLoop" rel="noopener noreferrer"&gt;Mojo::IOLoop&lt;/a&gt;, &lt;a href="https://metacpan.org/pod/AnyEvent" rel="noopener noreferrer"&gt;AnyEvent&lt;/a&gt;, &lt;a href="https://metacpan.org/pod/UV" rel="noopener noreferrer"&gt;UV&lt;/a&gt;. This diversity is a strength - different loops have different strengths - but it creates a problem for library authors. If you write an async Redis client, which event loop do you target?&lt;/p&gt;

&lt;p&gt;&lt;a href="https://metacpan.org/pod/Future::IO" rel="noopener noreferrer"&gt;Future::IO&lt;/a&gt; solves this by providing an abstraction layer. Libraries code against Future::IO's API (&lt;code&gt;sleep&lt;/code&gt;, &lt;code&gt;read&lt;/code&gt;, &lt;code&gt;write&lt;/code&gt;), and Future::IO delegates to whatever event loop is actually running. This means libraries like &lt;a href="https://metacpan.org/pod/Async::Redis" rel="noopener noreferrer"&gt;Async::Redis&lt;/a&gt; work with &lt;em&gt;any&lt;/em&gt; event loop - IO::Async, UV, or anything else that has a Future::IO backend.&lt;/p&gt;

&lt;p&gt;The catch: Future::IO needs to be told which backend to use. It doesn't auto-detect the running loop. Someone has to explicitly configure it:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight perl"&gt;&lt;code&gt;&lt;span class="k"&gt;use&lt;/span&gt; &lt;span class="nn"&gt;Future::&lt;/span&gt;&lt;span class="nv"&gt;IO&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="nn"&gt;Future::&lt;/span&gt;&lt;span class="nv"&gt;IO&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="nv"&gt;load_impl&lt;/span&gt;&lt;span class="p"&gt;('&lt;/span&gt;&lt;span class="s1"&gt;IOAsync&lt;/span&gt;&lt;span class="p"&gt;');&lt;/span&gt;  &lt;span class="c1"&gt;# Tell Future::IO to use IO::Async&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This configuration must happen before any Future::IO operations are attempted. Get it wrong - or forget it entirely - and things break in confusing ways.&lt;/p&gt;

&lt;h2&gt;
  
  
  The Problem
&lt;/h2&gt;

&lt;p&gt;Given that Future::IO needs explicit configuration, who should do it? Someone has to call:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight perl"&gt;&lt;code&gt;&lt;span class="k"&gt;use&lt;/span&gt; &lt;span class="nn"&gt;Future::&lt;/span&gt;&lt;span class="nv"&gt;IO&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="nn"&gt;Future::&lt;/span&gt;&lt;span class="nv"&gt;IO&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="nv"&gt;load_impl&lt;/span&gt;&lt;span class="p"&gt;('&lt;/span&gt;&lt;span class="s1"&gt;IOAsync&lt;/span&gt;&lt;span class="p"&gt;');&lt;/span&gt;  &lt;span class="c1"&gt;# or 'UV', etc.&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The question is: who?&lt;/p&gt;

&lt;h2&gt;
  
  
  What Doesn't Work
&lt;/h2&gt;

&lt;p&gt;A quick note: PAGI is built on the async Perl ecosystem created largely by Paul Evans (LeoNerd) - &lt;a href="https://metacpan.org/pod/IO::Async" rel="noopener noreferrer"&gt;IO::Async&lt;/a&gt;, &lt;a href="https://metacpan.org/pod/Future" rel="noopener noreferrer"&gt;Future&lt;/a&gt;, and &lt;a href="https://metacpan.org/pod/Future::IO" rel="noopener noreferrer"&gt;Future::IO&lt;/a&gt;. His guidance shaped our thinking here, and the constraints he identified are real engineering concerns, not arbitrary rules.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Libraries configuring Future::IO&lt;/strong&gt; - This was our first instinct. Async::Redis could call &lt;code&gt;Future::IO-&amp;gt;load_best_impl&lt;/code&gt; to auto-detect the best backend. But &lt;code&gt;load_best_impl&lt;/code&gt; picks based on what's &lt;em&gt;installed&lt;/em&gt;, not what's &lt;em&gt;running&lt;/em&gt;. If you have UV installed but you're running under IO::Async (like PAGI::Server), you get a mismatch. Things explode.&lt;/p&gt;

&lt;p&gt;Paul's guidance: libraries shouldn't configure Future::IO because they don't know the runtime context. This makes sense - a library is a guest in someone else's application.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;PAGI::Server (the module) configuring it&lt;/strong&gt; - We tried this too. It seemed logical - PAGI::Server runs IO::Async, so it knows the context. But there's a legitimate concern: PAGI::Server is still a module being &lt;code&gt;use&lt;/code&gt;d, and someone might embed it in a larger application.&lt;/p&gt;

&lt;p&gt;Consider: you're running PAGI::Server alongside other event-driven code - maybe a metrics collector, a background job processor, or a connection to a message queue. Your main script creates the IO::Async loop and adds multiple notifiers to it:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight perl"&gt;&lt;code&gt;&lt;span class="k"&gt;my&lt;/span&gt; &lt;span class="nv"&gt;$loop&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nn"&gt;IO::Async::&lt;/span&gt;&lt;span class="nv"&gt;Loop&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="k"&gt;new&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="c1"&gt;# Your other async code&lt;/span&gt;
&lt;span class="nv"&gt;$loop&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="nv"&gt;add&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nv"&gt;$metrics_reporter&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="nv"&gt;$loop&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="nv"&gt;add&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nv"&gt;$job_processor&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

&lt;span class="c1"&gt;# PAGI::Server is just one component&lt;/span&gt;
&lt;span class="k"&gt;my&lt;/span&gt; &lt;span class="nv"&gt;$server&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nn"&gt;PAGI::&lt;/span&gt;&lt;span class="nv"&gt;Server&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="k"&gt;new&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;app&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="nv"&gt;$app&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s"&gt;port&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="mi"&gt;8080&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="nv"&gt;$loop&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="nv"&gt;add&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nv"&gt;$server&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

&lt;span class="nv"&gt;$loop&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="nv"&gt;run&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;In this scenario, the main script is the entry point, not PAGI::Server. If PAGI::Server configured Future::IO, it would be a module reaching out to modify global state - potentially conflicting with configuration the main script already set up. The principle that only entry points should configure global state exists precisely for cases like this.&lt;/p&gt;

&lt;h2&gt;
  
  
  The Tradeoffs We Considered
&lt;/h2&gt;

&lt;h3&gt;
  
  
  Option 1: Apps Configure Future::IO
&lt;/h3&gt;

&lt;p&gt;The "pure" approach - make apps explicit about their dependencies:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight perl"&gt;&lt;code&gt;&lt;span class="c1"&gt;# app.pl&lt;/span&gt;
&lt;span class="k"&gt;use&lt;/span&gt; &lt;span class="nn"&gt;Future::&lt;/span&gt;&lt;span class="nv"&gt;IO&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="nn"&gt;Future::&lt;/span&gt;&lt;span class="nv"&gt;IO&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="nv"&gt;load_impl&lt;/span&gt;&lt;span class="p"&gt;('&lt;/span&gt;&lt;span class="s1"&gt;IOAsync&lt;/span&gt;&lt;span class="p"&gt;');&lt;/span&gt;

&lt;span class="k"&gt;use&lt;/span&gt; &lt;span class="nn"&gt;Async::&lt;/span&gt;&lt;span class="nv"&gt;Redis&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="c1"&gt;# ...&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;Pros:&lt;/strong&gt; Explicit, no magic, apps declare what they need.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Cons:&lt;/strong&gt; This fundamentally breaks PAGI's value proposition. The whole point of PAGI is write-once portability - your app should run under any PAGI-compliant server. If your app.pl says &lt;code&gt;load_impl('IOAsync')&lt;/code&gt;, what happens when you want to run under &lt;a href="https://metacpan.org/pod/Conduit" rel="noopener noreferrer"&gt;Conduit&lt;/a&gt; (Paul's Future::IO-native web server that currently supports PSGI and may support PAGI in the future)? You'd have to change your app code to switch servers. That's exactly what PAGI exists to prevent.&lt;/p&gt;

&lt;p&gt;I kept coming back to this. Boilerplate is annoying but tolerable. Breaking server-agnosticism is a dealbreaker.&lt;/p&gt;

&lt;h3&gt;
  
  
  Option 2: Extend the PAGI Spec
&lt;/h3&gt;

&lt;p&gt;We could add async capabilities directly to the PAGI spec - have servers provide event loop primitives through the scope:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight perl"&gt;&lt;code&gt;&lt;span class="c1"&gt;# Hypothetical: server provides async capabilities&lt;/span&gt;
&lt;span class="nv"&gt;$scope&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="p"&gt;{'&lt;/span&gt;&lt;span class="s1"&gt;pagi.sleep&lt;/span&gt;&lt;span class="p"&gt;'}&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;sub &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="o"&gt;...&lt;/span&gt; &lt;span class="p"&gt;};&lt;/span&gt;
&lt;span class="nv"&gt;$scope&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="p"&gt;{'&lt;/span&gt;&lt;span class="s1"&gt;pagi.timeout&lt;/span&gt;&lt;span class="p"&gt;'}&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;sub &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="o"&gt;...&lt;/span&gt; &lt;span class="p"&gt;};&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;Pros:&lt;/strong&gt; Clean abstraction. Apps use spec-defined capabilities, servers implement them however they want. True portability.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Cons:&lt;/strong&gt; Where does it stop? PAGI is modeled after Python's ASGI, which is deliberately minimal - it defines the request/response lifecycle and nothing more. Once you add sleep, what about DNS resolution? Database connections? HTTP clients?&lt;/p&gt;

&lt;p&gt;The spec becomes a grab-bag of async primitives, and every PAGI server implementation has to support all of them. That's a maintenance burden that discourages new server implementations. ASGI's success comes partly from its simplicity - implementing a basic ASGI server is tractable.&lt;/p&gt;

&lt;h3&gt;
  
  
  Option 3: Entry Point Configures
&lt;/h3&gt;

&lt;p&gt;Have &lt;code&gt;pagi-server&lt;/code&gt; (the CLI script, not the module) configure Future::IO. The script &lt;em&gt;is&lt;/em&gt; the application entry point.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Pros:&lt;/strong&gt; Follows the "only entry points configure" principle. CLI users get zero-config experience.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Cons:&lt;/strong&gt; Still feels like action at distance. Programmatic users of PAGI::Server must configure it themselves.&lt;/p&gt;

&lt;h2&gt;
  
  
  What We Chose
&lt;/h2&gt;

&lt;p&gt;We went with Option 3. In &lt;code&gt;pagi-server&lt;/code&gt;:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight perl"&gt;&lt;code&gt;&lt;span class="c1"&gt;# In PAGI::Runner (called by pagi-server)&lt;/span&gt;
&lt;span class="k"&gt;sub &lt;/span&gt;&lt;span class="nf"&gt;_configure_future_io&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;my&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nv"&gt;$self&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nv"&gt;@_&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

    &lt;span class="k"&gt;my&lt;/span&gt; &lt;span class="nv"&gt;$configured&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nb"&gt;eval&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="nb"&gt;require&lt;/span&gt; &lt;span class="nn"&gt;Future::IO::Impl::&lt;/span&gt;&lt;span class="nv"&gt;IOAsync&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
        &lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
    &lt;span class="p"&gt;};&lt;/span&gt;

    &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nv"&gt;$configured&lt;/span&gt; &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt; &lt;span class="nv"&gt;$self&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="nv"&gt;mode&lt;/span&gt; &lt;span class="ow"&gt;ne&lt;/span&gt; &lt;span class="p"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;production&lt;/span&gt;&lt;span class="p"&gt;'&lt;/span&gt; &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt; &lt;span class="o"&gt;!&lt;/span&gt;&lt;span class="nv"&gt;$self&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="nv"&gt;quiet&lt;/span&gt;&lt;span class="p"&gt;})&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="nb"&gt;warn&lt;/span&gt; &lt;span class="p"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;Future::IO configured for IO::Async&lt;/span&gt;&lt;span class="se"&gt;\n&lt;/span&gt;&lt;span class="p"&gt;";&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The result:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;&lt;code&gt;pagi-server myapp.pl&lt;/code&gt;&lt;/strong&gt; - Future::IO auto-configured, Async::Redis and other Future::IO libraries just work&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;&lt;code&gt;PAGI::Server-&amp;gt;new(...)&lt;/code&gt; programmatically&lt;/strong&gt; - You configure Future::IO yourself&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;For programmatic usage, here's what that looks like:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight perl"&gt;&lt;code&gt;&lt;span class="c1"&gt;#!/usr/bin/env perl&lt;/span&gt;
&lt;span class="k"&gt;use&lt;/span&gt; &lt;span class="nv"&gt;strict&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="k"&gt;use&lt;/span&gt; &lt;span class="nv"&gt;warnings&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="k"&gt;use&lt;/span&gt; &lt;span class="nn"&gt;IO::Async::&lt;/span&gt;&lt;span class="nv"&gt;Loop&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="c1"&gt;# Configure Future::IO BEFORE loading libraries that use it&lt;/span&gt;
&lt;span class="k"&gt;use&lt;/span&gt; &lt;span class="nn"&gt;Future::&lt;/span&gt;&lt;span class="nv"&gt;IO&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="nn"&gt;Future::&lt;/span&gt;&lt;span class="nv"&gt;IO&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="nv"&gt;load_impl&lt;/span&gt;&lt;span class="p"&gt;('&lt;/span&gt;&lt;span class="s1"&gt;IOAsync&lt;/span&gt;&lt;span class="p"&gt;');&lt;/span&gt;

&lt;span class="c1"&gt;# Now these work correctly&lt;/span&gt;
&lt;span class="k"&gt;use&lt;/span&gt; &lt;span class="nn"&gt;Async::&lt;/span&gt;&lt;span class="nv"&gt;Redis&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="k"&gt;use&lt;/span&gt; &lt;span class="nn"&gt;PAGI::&lt;/span&gt;&lt;span class="nv"&gt;Server&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="k"&gt;my&lt;/span&gt; &lt;span class="nv"&gt;$redis&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nn"&gt;Async::&lt;/span&gt;&lt;span class="nv"&gt;Redis&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="k"&gt;new&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;host&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;localhost&lt;/span&gt;&lt;span class="p"&gt;');&lt;/span&gt;

&lt;span class="k"&gt;my&lt;/span&gt; &lt;span class="nv"&gt;$app&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;sub &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;my&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nv"&gt;$scope&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nv"&gt;$receive&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nv"&gt;$send&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nv"&gt;@_&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
    &lt;span class="c1"&gt;# ... your app using $redis&lt;/span&gt;
&lt;span class="p"&gt;};&lt;/span&gt;

&lt;span class="k"&gt;my&lt;/span&gt; &lt;span class="nv"&gt;$loop&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nn"&gt;IO::Async::&lt;/span&gt;&lt;span class="nv"&gt;Loop&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="k"&gt;new&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="k"&gt;my&lt;/span&gt; &lt;span class="nv"&gt;$server&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nn"&gt;PAGI::&lt;/span&gt;&lt;span class="nv"&gt;Server&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="k"&gt;new&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;app&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="nv"&gt;$app&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s"&gt;port&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="mi"&gt;8080&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="nv"&gt;$loop&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="nv"&gt;add&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nv"&gt;$server&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="nv"&gt;$server&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="nb"&gt;listen&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="nv"&gt;get&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="nv"&gt;$loop&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="nv"&gt;run&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;We're comfortable with this tradeoff. If you're writing your own server orchestration - creating loops, adding notifiers, managing the lifecycle - you're already in "I know what I'm doing" territory. You're the kind of developer who reads documentation about event loop integration. Adding two lines to configure Future::IO is not a burden for someone already writing fifteen lines of loop setup.&lt;/p&gt;

&lt;p&gt;The CLI user who just wants &lt;code&gt;pagi-server app.pl&lt;/code&gt; to work shouldn't need to know any of this. The power user embedding PAGI::Server in a custom harness can handle it. (See the &lt;a href="https://metacpan.org/pod/PAGI::Server#LOOP-INTEROPERABILITY" rel="noopener noreferrer"&gt;LOOP INTEROPERABILITY&lt;/a&gt; section in PAGI::Server docs.)&lt;/p&gt;

&lt;h2&gt;
  
  
  Why This Felt Right (Eventually)
&lt;/h2&gt;

&lt;p&gt;I had doubts. Isn't this still "magic"? Isn't the app implicitly depending on server configuration?&lt;/p&gt;

&lt;p&gt;But here's the reframing that helped: &lt;strong&gt;configuring the async runtime is the server's job&lt;/strong&gt;. Python's ASGI servers don't ask users to "configure asyncio" - the runtime just works. The server provides the execution environment.&lt;/p&gt;

&lt;p&gt;The key insight is that this isn't about any single library. It's about the entire Future::IO ecosystem - database drivers, HTTP clients, Redis, message queues, and anything else built on Future::IO. These libraries represent the future of async Perl. If every PAGI user has to understand Future::IO configuration internals before using any of them, that's a barrier to adoption that hurts the whole ecosystem.&lt;/p&gt;

&lt;p&gt;By having &lt;code&gt;pagi-server&lt;/code&gt; configure Future::IO, we're saying: "When you run under pagi-server, Future::IO libraries work. That's part of what the server provides." It's similar to how Python's ASGI servers provide an asyncio environment - you don't configure the async runtime, you just use libraries that depend on it.&lt;/p&gt;

&lt;h2&gt;
  
  
  Also in This Release: SSE Query Parameters
&lt;/h2&gt;

&lt;p&gt;PAGI::SSE now has query parameter parsing, matching PAGI::Request and PAGI::WebSocket:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight perl"&gt;&lt;code&gt;&lt;span class="k"&gt;my&lt;/span&gt; &lt;span class="nv"&gt;$sse&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nn"&gt;PAGI::&lt;/span&gt;&lt;span class="nv"&gt;SSE&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="k"&gt;new&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nv"&gt;$scope&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nv"&gt;$receive&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nv"&gt;$send&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

&lt;span class="k"&gt;my&lt;/span&gt; &lt;span class="nv"&gt;$channel&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nv"&gt;$sse&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="nv"&gt;query_param&lt;/span&gt;&lt;span class="p"&gt;('&lt;/span&gt;&lt;span class="s1"&gt;channel&lt;/span&gt;&lt;span class="p"&gt;');&lt;/span&gt;
&lt;span class="k"&gt;my&lt;/span&gt; &lt;span class="nv"&gt;$params&lt;/span&gt;  &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nv"&gt;$sse&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="nv"&gt;query_params&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;  &lt;span class="c1"&gt;# Hash::MultiValue&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This is straightforward consistency work - all three scope types now have the same query parameter API.&lt;/p&gt;

&lt;h2&gt;
  
  
  This Is Experimental - RFC Welcome
&lt;/h2&gt;

&lt;p&gt;I want to be clear: &lt;strong&gt;the Future::IO integration in this release is experimental&lt;/strong&gt;. We've made a pragmatic choice that works, but I'm not certain it's the right long-term answer.&lt;/p&gt;

&lt;p&gt;The PAGI spec will probably need to address async capability configuration at some point. I'm just not sure what that should look like yet. Should servers declare what async backend they use? Should there be a standard way for apps to discover capabilities? Should Future::IO configuration be part of the lifespan protocol?&lt;/p&gt;

&lt;p&gt;I'm treating this release as a request for comments. If you have thoughts on:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Whether &lt;code&gt;pagi-server&lt;/code&gt; auto-configuring Future::IO is the right call&lt;/li&gt;
&lt;li&gt;How the PAGI spec should handle async ecosystem integration&lt;/li&gt;
&lt;li&gt;Alternative approaches we haven't considered&lt;/li&gt;
&lt;li&gt;Pain points you hit when using Future::IO libraries with PAGI&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;...I want to hear them. Please leave your thoughts on &lt;a href="https://github.com/jjn1056/pagi/issues/33" rel="noopener noreferrer"&gt;GitHub issue #33&lt;/a&gt;, which I've opened specifically for this discussion.&lt;/p&gt;




&lt;p&gt;&lt;em&gt;PAGI is a PSGI-like specification for async Perl web applications. Feedback welcome at &lt;a href="https://github.com/jjn1056/pagi" rel="noopener noreferrer"&gt;github.com/jjn1056/pagi&lt;/a&gt;.&lt;/em&gt;&lt;/p&gt;

</description>
      <category>architecture</category>
      <category>devjournal</category>
      <category>opensource</category>
      <category>programming</category>
    </item>
    <item>
      <title>The #1 sentence I add to prompts that makes them better</title>
      <dc:creator>John Napiorkowski</dc:creator>
      <pubDate>Sun, 25 Jan 2026 00:16:51 +0000</pubDate>
      <link>https://dev.to/jjn1056/the-1-sentence-i-add-to-prompts-that-makes-them-better-514n</link>
      <guid>https://dev.to/jjn1056/the-1-sentence-i-add-to-prompts-that-makes-them-better-514n</guid>
      <description>&lt;h2&gt;
  
  
  Prompt Engineering or selling your soul?
&lt;/h2&gt;

&lt;p&gt;If you use AI at work, you’ve probably had this experience:&lt;/p&gt;

&lt;p&gt;You write a prompt that feels &lt;em&gt;clear enough&lt;/em&gt;, hit enter, and the model confidently produces something that is… technically fine… but not what you meant.&lt;/p&gt;

&lt;p&gt;Maybe it built the right thing in the wrong style.&lt;br&gt;
Maybe it chose an approach you would never ship.&lt;br&gt;
Maybe it made assumptions you didn’t realize you were leaving unstated.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;I’ve started treating prompts like a contract with the devil.&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Not because I think AI is evil — just because it’s &lt;em&gt;literal&lt;/em&gt;, opportunistic, and perfectly willing to sprint in the wrong direction if you give it even a small opening.&lt;/p&gt;

&lt;p&gt;And you can’t cover every edge case up front.&lt;/p&gt;

&lt;p&gt;So here’s the one sentence I add to a lot of my prompts that consistently makes the results better:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;Before you begin, ask any clarifying questions you need to fully understand what I’m asking and to do an excellent job.&lt;/strong&gt;&lt;/p&gt;
&lt;/blockquote&gt;

&lt;h2&gt;
  
  
  Why it works (and why it’s “vibe engineering”)
&lt;/h2&gt;

&lt;p&gt;Most prompting advice is basically: &lt;em&gt;be more specific.&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;That’s true, but it’s incomplete — because the whole problem is that you often &lt;strong&gt;don’t realize what you forgot to specify&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;This sentence flips the dynamic:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Instead of &lt;em&gt;“I describe something and hope the AI guesses right,”&lt;/em&gt;
&lt;/li&gt;
&lt;li&gt;it becomes &lt;em&gt;collaborative.&lt;/em&gt;
&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;It turns the model into a reviewer &lt;strong&gt;before&lt;/strong&gt; it becomes an implementer.&lt;/p&gt;

&lt;p&gt;And it forces the “unknown unknowns” to show up early, while it’s still cheap to correct.&lt;/p&gt;

&lt;h2&gt;
  
  
  The kind of questions that save you
&lt;/h2&gt;

&lt;p&gt;My favorite clarifying questions are the ones that expose missing context I didn’t realize mattered.&lt;/p&gt;

&lt;p&gt;Like:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;“Is there an existing system you want me to use as a template?”&lt;/li&gt;
&lt;li&gt;“Is this a large table / high-traffic database?”&lt;/li&gt;
&lt;li&gt;“Is this safe to run during business hours?”&lt;/li&gt;
&lt;li&gt;“What does success look like: correctness, speed, low risk, or minimal code changes?”&lt;/li&gt;
&lt;li&gt;“Do you care about test coverage, or just a working fix?”&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Those questions aren’t just helpful to the AI.&lt;/p&gt;

&lt;p&gt;They’re helpful to &lt;em&gt;me.&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;Because half the time, I’m using the AI to tease out details I forgot to include in the prompt in the first place.&lt;/p&gt;

&lt;h2&gt;
  
  
  A realistic failure mode this prevents
&lt;/h2&gt;

&lt;p&gt;Say you ask:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;“Write a migration to backfill X safely.”&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;The AI might happily generate a perfectly valid migration that:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;locks the table,&lt;/li&gt;
&lt;li&gt;runs as one big transaction,&lt;/li&gt;
&lt;li&gt;does a full scan,&lt;/li&gt;
&lt;li&gt;adds an index in a way that takes forever,&lt;/li&gt;
&lt;li&gt;and generally assumes the world is a small quiet sandbox.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;It’s not “wrong” — it’s just not something you want to discover &lt;em&gt;after&lt;/em&gt; you already committed to the approach.&lt;/p&gt;

&lt;p&gt;If you make the model interview you first, you’ll often get the question you forgot to say out loud:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;“How large is the table, and can this run on prod without blocking writes?”&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;That one question can save you a very annoying afternoon.&lt;/p&gt;

&lt;h2&gt;
  
  
  When I &lt;em&gt;don’t&lt;/em&gt; use it
&lt;/h2&gt;

&lt;p&gt;I don’t paste this sentence into every single prompt.&lt;/p&gt;

&lt;p&gt;If I’m asking something small and obvious (“write this one-liner”, “explain this error”, “rename these variables”), it’s unnecessary overhead.&lt;/p&gt;

&lt;p&gt;But if I’m doing any of these, it comes out almost automatically:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;writing something new (a real feature, not a snippet)&lt;/li&gt;
&lt;li&gt;debugging a complex problem&lt;/li&gt;
&lt;li&gt;anything with multiple moving parts&lt;/li&gt;
&lt;li&gt;anything where “technically correct” can still waste time&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Copy/paste template you can steal
&lt;/h2&gt;

&lt;p&gt;Here’s a version you can drop into your own prompts:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;My default prompt preamble&lt;/strong&gt;&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;Before you begin, ask any clarifying questions you need to fully understand what I’m asking and to do an excellent job.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;&lt;strong&gt;Optional follow-up (if you want to be extra explicit)&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;If something is ambiguous, &lt;strong&gt;don’t guess&lt;/strong&gt; — ask.&lt;/li&gt;
&lt;li&gt;If there are multiple valid approaches, &lt;strong&gt;list the options&lt;/strong&gt; and tell me what you recommend and why.&lt;/li&gt;
&lt;li&gt;After questions are answered, produce the output in a clean, usable format.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;That’s it.&lt;/p&gt;

&lt;p&gt;It’s not magic. It doesn’t make the model smarter.&lt;/p&gt;

&lt;p&gt;It just keeps the process collaborative instead of “write a prompt and hope for the best.”&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Teaser:&lt;/strong&gt; if you want to hear about the &lt;strong&gt;#2 sentence I add to prompts&lt;/strong&gt; to make them even better, get me to &lt;strong&gt;100 likes&lt;/strong&gt; 😄&lt;/p&gt;

</description>
      <category>ai</category>
      <category>programming</category>
      <category>promptengineering</category>
      <category>vibecoding</category>
    </item>
    <item>
      <title>PAGI::Server: Stress-Testing an Async Perl HTTP Server the Hard Way</title>
      <dc:creator>John Napiorkowski</dc:creator>
      <pubDate>Thu, 01 Jan 2026 21:52:45 +0000</pubDate>
      <link>https://dev.to/jjn1056/pagiserver-stress-testing-an-async-perl-http-server-the-hard-way-i2e</link>
      <guid>https://dev.to/jjn1056/pagiserver-stress-testing-an-async-perl-http-server-the-hard-way-i2e</guid>
      <description>&lt;p&gt;Over the last few days I’ve been stress-testing &lt;strong&gt;PAGI::Server&lt;/strong&gt;, an async HTTP server built on &lt;strong&gt;IO::Async&lt;/strong&gt;, &lt;strong&gt;Future::IO&lt;/strong&gt;, and an &lt;strong&gt;EV&lt;/strong&gt; event loop. The goal wasn’t to chase synthetic “requests per second” numbers, but to answer harder questions:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Does streaming actually behave correctly?&lt;/li&gt;
&lt;li&gt;What happens under sustained concurrency?&lt;/li&gt;
&lt;li&gt;Do slow clients cause buffer bloat?&lt;/li&gt;
&lt;li&gt;Does backpressure really work?&lt;/li&gt;
&lt;li&gt;How does it fail under overload?&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;This post summarizes what I learned by pushing the server well past “normal” workloads and watching it misbehave — or not.&lt;/p&gt;




&lt;h2&gt;
  
  
  Architecture in one paragraph
&lt;/h2&gt;

&lt;p&gt;PAGI::Server is:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;single-process, event-driven by default&lt;/li&gt;
&lt;li&gt;optionally multi-process via workers&lt;/li&gt;
&lt;li&gt;fully async end-to-end (no threads)&lt;/li&gt;
&lt;li&gt;streaming-first (responses are not buffered by default)&lt;/li&gt;
&lt;li&gt;designed to support ASGI-like semantics in Perl&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The server accepts connections in a main event loop and routes work through &lt;strong&gt;IO::Async::Stream&lt;/strong&gt; objects. Application code produces responses via a &lt;code&gt;$send&lt;/code&gt; callback that emits protocol events (&lt;code&gt;http.response.start&lt;/code&gt;, &lt;code&gt;http.response.body&lt;/code&gt;, etc.).&lt;/p&gt;

&lt;p&gt;That last part matters, because it’s where most async servers quietly cheat.&lt;/p&gt;

&lt;h2&gt;
  
  
  An important note: Although PAGI::Server is IO::Async based, that is an implementation detail, not a core requirement of the PAGI specification.  That said I build PAGI::Server on IO::Async because it's mature, battle tested and fully featured.
&lt;/h2&gt;

&lt;h2&gt;
  
  
  Baseline: fast responses
&lt;/h2&gt;

&lt;p&gt;Before touching streaming, I tested a trivial HTTP handler.&lt;/p&gt;

&lt;p&gt;On an older 16-core MacBook Pro:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;~22–23k requests/sec&lt;/li&gt;
&lt;li&gt;p50 latency ~20ms&lt;/li&gt;
&lt;li&gt;p99 &amp;lt; 50ms&lt;/li&gt;
&lt;li&gt;CPU mostly idle&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Nothing surprising here — just confirming there’s no accidental serialization or blocking in the accept loop.&lt;/p&gt;

&lt;p&gt;Just to set some baselines, I tested a simple, similar PSGI application using Starman, one of the most popular choices for serving PSGI apps, and I found it topped out at around 16K requests/sec, with significantly worse latency spread and dropped connections.&lt;/p&gt;




&lt;h2&gt;
  
  
  Streaming test: 2-second responses, 500 concurrent clients
&lt;/h2&gt;

&lt;p&gt;Next, I switched to a streaming handler that sends chunks over ~2 seconds.&lt;/p&gt;

&lt;p&gt;With &lt;strong&gt;500 concurrent connections&lt;/strong&gt;, single process:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;~242–245 requests/sec&lt;/li&gt;
&lt;li&gt;p50 ≈ 2.00s&lt;/li&gt;
&lt;li&gt;p99 ≈ 2.3–2.4s&lt;/li&gt;
&lt;li&gt;CPU ~10–20% busy&lt;/li&gt;
&lt;li&gt;memory stable&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;This matters because the math checks out:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;500 concurrent / 2 seconds ≈ 250 rps theoretical max
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Hitting ~97–98% of theoretical max strongly suggests:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;no head-of-line blocking&lt;/li&gt;
&lt;li&gt;no per-connection threads&lt;/li&gt;
&lt;li&gt;no accidental buffering&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The server is doing what an async server should do: waiting, not working.&lt;/p&gt;




&lt;h2&gt;
  
  
  A gotcha: TTY logging will ruin your benchmark
&lt;/h2&gt;

&lt;p&gt;One early run looked worse than expected. The cause was embarrassingly simple:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;Access logging was printing to a terminal (TTY).&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;At ~20k+ log lines/sec, the terminal became the bottleneck.&lt;/p&gt;

&lt;p&gt;Once logging was disabled or redirected:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;CPU usage dropped dramatically&lt;/li&gt;
&lt;li&gt;latency tails tightened&lt;/li&gt;
&lt;li&gt;throughput returned to theoretical limits&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Lesson: &lt;strong&gt;never benchmark with synchronous TTY logging enabled&lt;/strong&gt;.&lt;/p&gt;




&lt;h2&gt;
  
  
  Overload behavior: what happens at 2500 concurrent streams?
&lt;/h2&gt;

&lt;p&gt;With &lt;code&gt;ulimit -n = 2048&lt;/code&gt;, single-process PAGI::Server started rejecting connections around ~2000 open sockets.&lt;/p&gt;

&lt;p&gt;That rejection path returned:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;&lt;code&gt;503 Service Unavailable&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;code&gt;Retry-After: 5&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;code&gt;Connection: close&lt;/code&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;This is &lt;strong&gt;good behavior&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;Instead of accepting everything and melting down, the server applied &lt;em&gt;admission control&lt;/em&gt; based on available file descriptors.&lt;/p&gt;

&lt;p&gt;The client (&lt;code&gt;hey&lt;/code&gt;) complained with “unsolicited response” warnings — a known artifact when clients aggressively reuse keep-alive connections under churn. The server was behaving correctly.&lt;/p&gt;




&lt;h2&gt;
  
  
  Scaling out: 16 workers
&lt;/h2&gt;

&lt;p&gt;With &lt;strong&gt;16 worker processes&lt;/strong&gt;, the same workload:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;sustained ~1200 requests/sec at 2500 concurrency&lt;/li&gt;
&lt;li&gt;p50 ≈ 2.00s&lt;/li&gt;
&lt;li&gt;p99 ≈ 3.0s&lt;/li&gt;
&lt;li&gt;all 200 responses, no rejects&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Again, the math checks:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;2500 concurrent / 2 seconds ≈ 1250 rps theoretical
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Result: ~96% of theoretical max.&lt;/p&gt;

&lt;p&gt;This validated that PAGI’s worker model increases capacity cleanly without breaking streaming semantics.&lt;/p&gt;




&lt;h2&gt;
  
  
  The real test: slow clients and backpressure
&lt;/h2&gt;

&lt;p&gt;Throughput benchmarks are easy. &lt;strong&gt;Backpressure is not.&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;To test this, I used a stress app that streams tens of megabytes as fast as possible, then ran clients with artificial throttling.&lt;/p&gt;

&lt;h3&gt;
  
  
  Slow client test
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;curl &lt;span class="nt"&gt;--limit-rate&lt;/span&gt; 1M http://localhost:5000/stream/50
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Observed:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;~50MB transferred in ~49 seconds&lt;/li&gt;
&lt;li&gt;download rate stayed near 1MB/s&lt;/li&gt;
&lt;li&gt;server did &lt;strong&gt;not&lt;/strong&gt; buffer the entire response&lt;/li&gt;
&lt;li&gt;no memory blow-up&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;This is the critical signal.&lt;/p&gt;

&lt;p&gt;If backpressure were broken, the server would buffer 50MB instantly and curl would “catch up” later. That did not happen.&lt;/p&gt;




&lt;h2&gt;
  
  
  High-throughput clients
&lt;/h2&gt;

&lt;p&gt;Unthrottled clients on localhost achieved:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;~75–80 MB/s per connection&lt;/li&gt;
&lt;li&gt;linear scaling until loopback bandwidth became the limit&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Again, no red flags.&lt;/p&gt;




&lt;h2&gt;
  
  
  Concurrency + large bodies
&lt;/h2&gt;

&lt;p&gt;Using &lt;code&gt;hey&lt;/code&gt; with 50 concurrent clients streaming 10–50MB bodies:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;high aggregate throughput&lt;/li&gt;
&lt;li&gt;wide latency distribution&lt;/li&gt;
&lt;li&gt;no error rates&lt;/li&gt;
&lt;li&gt;no collapse&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;At this point, the bottleneck was clearly:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;kernel socket buffers&lt;/li&gt;
&lt;li&gt;memory copy bandwidth&lt;/li&gt;
&lt;li&gt;client tooling limits&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Not the server itself.&lt;/p&gt;




&lt;h2&gt;
  
  
  The subtle risk we identified (and addressed)
&lt;/h2&gt;

&lt;p&gt;While reviewing the server code, one important detail surfaced:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;writes use &lt;code&gt;IO::Async::Stream-&amp;gt;write&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;writes enqueue data into an outgoing buffer&lt;/li&gt;
&lt;li&gt;there is no implicit send-side backpressure unless implemented explicitly&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;This is fine for polite producers (like most apps), but dangerous if:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;an app writes huge chunks rapidly&lt;/li&gt;
&lt;li&gt;the client reads slowly&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;To address this, we added &lt;strong&gt;send-side backpressure&lt;/strong&gt;:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;track queued outgoing bytes&lt;/li&gt;
&lt;li&gt;define high / low watermarks&lt;/li&gt;
&lt;li&gt;pause &lt;code&gt;$send-&amp;gt;(...)&lt;/code&gt; futures when buffers exceed the high watermark&lt;/li&gt;
&lt;li&gt;resume when the buffer drains&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;This change makes slow-client behavior &lt;em&gt;safe by default&lt;/em&gt;.&lt;/p&gt;




&lt;h2&gt;
  
  
  What I did &lt;em&gt;not&lt;/em&gt; see (important)
&lt;/h2&gt;

&lt;p&gt;Across all tests, I did &lt;strong&gt;not&lt;/strong&gt; observe:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;unbounded RSS growth&lt;/li&gt;
&lt;li&gt;runaway CPU&lt;/li&gt;
&lt;li&gt;increasing latency under steady load&lt;/li&gt;
&lt;li&gt;dropped connections without errors&lt;/li&gt;
&lt;li&gt;event-loop starvation&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Those are the usual failure modes of async servers. None appeared.&lt;/p&gt;




&lt;h2&gt;
  
  
  Final assessment
&lt;/h2&gt;

&lt;p&gt;Based on these tests:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;PAGI::Server handles streaming correctly&lt;/li&gt;
&lt;li&gt;backpressure works (and is now explicit)&lt;/li&gt;
&lt;li&gt;overload fails fast and cleanly&lt;/li&gt;
&lt;li&gt;worker scaling behaves predictably&lt;/li&gt;
&lt;li&gt;performance matches theoretical limits&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;There’s still more to do — especially long-duration soak tests and production-grade observability — but the fundamentals are solid.&lt;/p&gt;

&lt;p&gt;Most importantly, &lt;strong&gt;the server behaves honestly&lt;/strong&gt;. It doesn’t fake async by buffering everything and hoping for the best.&lt;/p&gt;

</description>
      <category>architecture</category>
      <category>backend</category>
      <category>testing</category>
      <category>performance</category>
    </item>
    <item>
      <title>Perl PAGI Project Update</title>
      <dc:creator>John Napiorkowski</dc:creator>
      <pubDate>Sun, 28 Dec 2025 17:14:55 +0000</pubDate>
      <link>https://dev.to/jjn1056/perl-pagi-project-update-2n2p</link>
      <guid>https://dev.to/jjn1056/perl-pagi-project-update-2n2p</guid>
      <description>&lt;h1&gt;
  
  
  What We Learned Shipping PAGI
&lt;/h1&gt;

&lt;p&gt;&lt;a href="https://metacpan.org/release/JJNAPIORK/PAGI-0.001011" rel="noopener noreferrer"&gt;PAGI&lt;/a&gt; (Perl Asynchronous Gateway Interface) is a new web specification and reference server for Perl, designed to bring first-class async/await support to web development. Think of it as Perl's answer to Python's ASGI - a modern foundation for WebSocket, Server-Sent Events, and HTTP applications using &lt;a href="https://metacpan.org/pod/Future" rel="noopener noreferrer"&gt;Future&lt;/a&gt; and &lt;a href="https://metacpan.org/pod/Future::AsyncAwait" rel="noopener noreferrer"&gt;Future::AsyncAwait&lt;/a&gt; syntax.&lt;/p&gt;

&lt;p&gt;Since the first stable release on December 24th, we've shipped seven releases in four days. This pace wasn't planned - it emerged from squashing bugs reported by &lt;a href="http://www.cpantesters.org/" rel="noopener noreferrer"&gt;CPAN Testers&lt;/a&gt;, especially on important but less common platforms like FreeBSD, along with rapid iteration on the API. Here's what we learned along the way.&lt;/p&gt;

&lt;h2&gt;
  
  
  Sometimes the Right Code is No Code
&lt;/h2&gt;

&lt;p&gt;One of the more interesting decisions we made was &lt;em&gt;removing&lt;/em&gt; a feature: built-in sendfile support.&lt;/p&gt;

&lt;p&gt;The initial implementation used &lt;a href="https://metacpan.org/pod/IO::Async::FileStream" rel="noopener noreferrer"&gt;IO::Async::FileStream&lt;/a&gt; with &lt;a href="https://metacpan.org/pod/Sys::Sendfile" rel="noopener noreferrer"&gt;Sys::Sendfile&lt;/a&gt; to efficiently stream large files directly from disk to socket, bypassing userspace copying. It worked beautifully on Linux. Then we tested on FreeBSD.&lt;/p&gt;

&lt;p&gt;The problem wasn't just FreeBSD - it was the interaction between sendfile and non-blocking sockets, edge cases with partial writes, and the complexity of handling all the ways different operating systems implement (or don't implement) zero-copy file transfers. We found ourselves writing increasingly elaborate platform-specific workarounds.&lt;/p&gt;

&lt;p&gt;We stepped back and asked: what problem are we actually solving? In production, large file serving should go through a reverse proxy anyway. Nginx's X-Sendfile (or X-Accel-Redirect) and Apache's mod_xsendfile exist precisely for this use case. They're battle-tested, optimized, and someone else maintains them.&lt;/p&gt;

&lt;p&gt;So we removed sendfile entirely and created &lt;a href="https://metacpan.org/pod/PAGI::Middleware::XSendfile" rel="noopener noreferrer"&gt;PAGI::Middleware::XSendfile&lt;/a&gt; instead. Your PAGI application returns a special header, and your reverse proxy handles the actual file transfer. The Unix philosophy wins again: do one thing well, and compose with specialized tools.&lt;/p&gt;

&lt;p&gt;That said, this isn't ideological. If someone wants to contribute a robust, cross-platform sendfile implementation, we'd welcome the PR. The current solution is pragmatic, not dogmatic.&lt;/p&gt;

&lt;h2&gt;
  
  
  Async Error Handling Done Right
&lt;/h2&gt;

&lt;p&gt;Asynchronous programming has a dirty secret: errors love to disappear.&lt;/p&gt;

&lt;p&gt;In synchronous code, an exception bubbles up naturally. In async code with fire-and-forget Futures, exceptions can vanish into the void. Your background task fails, nobody notices, and you spend hours wondering why something silently stopped working.&lt;/p&gt;

&lt;p&gt;PAGI originally used a pattern called &lt;code&gt;retain()&lt;/code&gt; to keep Futures alive:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight perl"&gt;&lt;code&gt;&lt;span class="nv"&gt;$self&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="nv"&gt;retain&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nv"&gt;$self&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="nv"&gt;some_async_operation&lt;/span&gt;&lt;span class="p"&gt;());&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This worked for keeping the Future from being garbage collected, but it had a problem: if &lt;code&gt;some_async_operation()&lt;/code&gt; failed, the error was quietly swallowed. The Future failed, nobody was listening, and life went on - except for the bug you didn't know about.&lt;/p&gt;

&lt;p&gt;We replaced this with &lt;code&gt;adopt_future()&lt;/code&gt;:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight perl"&gt;&lt;code&gt;&lt;span class="nv"&gt;$self&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="nv"&gt;adopt_future&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nv"&gt;$self&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="nv"&gt;some_async_operation&lt;/span&gt;&lt;span class="p"&gt;());&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The difference is crucial. When an adopted Future fails, the error propagates to the parent context and gets logged. You see it. You can debug it. The failure is observable.&lt;/p&gt;

&lt;p&gt;This applies throughout PAGI's internals - connection handling, protocol parsing, worker lifecycle management. Errors that previously might have disappeared now surface properly. It's a small API change that dramatically improves debuggability.&lt;/p&gt;

&lt;p&gt;For application developers, the pattern extends to your own code. When you spawn background work in a request handler, adopt it rather than retaining it. Your future self debugging a production issue will thank you.&lt;/p&gt;

&lt;h2&gt;
  
  
  Making PAGI Feel Like Perl
&lt;/h2&gt;

&lt;p&gt;One principle guided several of our developer experience decisions: PAGI should feel like Perl, not like a foreign framework that happens to run on Perl.&lt;/p&gt;

&lt;p&gt;The clearest example is the new &lt;code&gt;-e&lt;/code&gt; and &lt;code&gt;-M&lt;/code&gt; flags for &lt;code&gt;pagi-server&lt;/code&gt;:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="c"&gt;# Inline app, just like perl -e&lt;/span&gt;
pagi-server &lt;span class="nt"&gt;-e&lt;/span&gt; &lt;span class="s1"&gt;'sub { my ($scope, $receive, $send) = @_; ... }'&lt;/span&gt;

&lt;span class="c"&gt;# Load modules first, like perl -M&lt;/span&gt;
pagi-server &lt;span class="nt"&gt;-MPAGI&lt;/span&gt;::App::File &lt;span class="nt"&gt;-e&lt;/span&gt; &lt;span class="s1"&gt;'PAGI::App::File-&amp;gt;new(root =&amp;gt; ".")-&amp;gt;to_app'&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;If you've used &lt;code&gt;perl -e&lt;/code&gt; for quick scripts, this syntax is immediately familiar. No ceremony, no boilerplate files for simple cases.&lt;/p&gt;

&lt;p&gt;This philosophy extends to other areas:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Test::Client&lt;/strong&gt; now traps exceptions by default, matching how Perl developers expect test failures to behave. Multi-value headers, query parameters, and form fields work naturally.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Environment modes&lt;/strong&gt; follow the pattern established by &lt;a href="https://metacpan.org/pod/Plack" rel="noopener noreferrer"&gt;Plack&lt;/a&gt;: &lt;code&gt;-E development&lt;/code&gt; enables debugging middleware, &lt;code&gt;-E production&lt;/code&gt; optimizes for performance. The detection is automatic based on TTY.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Signal handling&lt;/strong&gt; follows Unix conventions. SIGTERM and SIGINT trigger graceful shutdown. SIGHUP reloads workers. SIGTTIN/SIGTTOU adjust worker counts. It behaves like the system tools Perl developers already know.&lt;/p&gt;

&lt;p&gt;The goal is reducing cognitive overhead. You shouldn't need to learn "the PAGI way" for things Perl and Unix already have conventions for.&lt;/p&gt;

&lt;h2&gt;
  
  
  What's Next
&lt;/h2&gt;

&lt;p&gt;PAGI is stabilizing, but there's plenty of interesting work ahead.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Documentation has been a major focus.&lt;/strong&gt; We split the original Tutorial into two documents: a &lt;a href="https://metacpan.org/pod/PAGI::Tutorial" rel="noopener noreferrer"&gt;Tutorial&lt;/a&gt; for getting started with core concepts, and a &lt;a href="https://metacpan.org/pod/PAGI::Cookbook" rel="noopener noreferrer"&gt;Cookbook&lt;/a&gt; for advanced patterns like background tasks, database connection pooling, JWT authentication, and Redis-backed sessions. The goal is progressive disclosure - you shouldn't need to read about WebSocket heartbeat strategies before you've served your first HTTP response.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;The middleware collection&lt;/strong&gt; continues to grow. We have 37 middleware components now (authentication, rate limiting, CORS, sessions, and more), but there's always room for more.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;We're considering a repository split&lt;/strong&gt; and would love community feedback. Currently PAGI ships everything in one distribution: core spec, reference server, 37 middleware components, 19 apps, and test utilities. This is convenient (&lt;code&gt;cpanm PAGI&lt;/code&gt; gets you everything) but means installing the full stack even if you only need parts.&lt;/p&gt;

&lt;p&gt;Options we're weighing:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Keep unified&lt;/strong&gt; (current) - simpler while the spec is still evolving&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Split server&lt;/strong&gt; - separate PAGI::Server as "reference implementation," enabling alternative servers&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Three-way split&lt;/strong&gt; - core spec, server, and contrib (middleware/apps) as separate distributions&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;What would serve you better? Lighter installs? Easier contribution workflow? Alternative server implementations? We'd appreciate hearing your use case.&lt;/p&gt;

&lt;p&gt;Most importantly, we're looking for contributors. The codebase is approachable - modern Perl with async/await, comprehensive tests, and documented internals. Whether you want to tackle something meaty like cross-platform sendfile, or something focused like a new middleware component, there's room for your ideas.&lt;/p&gt;

&lt;h2&gt;
  
  
  Get Involved
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;GitHub:&lt;/strong&gt; &lt;a href="https://github.com/jjn1056/pagi" rel="noopener noreferrer"&gt;github.com/jjn1056/pagi&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;CPAN:&lt;/strong&gt; &lt;a href="https://metacpan.org/release/JJNAPIORK/PAGI-0.001011" rel="noopener noreferrer"&gt;PAGI on MetaCPAN&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;&lt;a href="https://github.com/jjn1056/pagi/blob/main/CONTRIBUTING.md" rel="noopener noreferrer"&gt;CONTRIBUTING.md&lt;/a&gt;:&lt;/strong&gt; Guidelines for contributors, including our approach to AI-assisted development&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;&lt;a href="https://github.com/jjn1056/pagi/blob/main/SECURITY.md" rel="noopener noreferrer"&gt;SECURITY.md&lt;/a&gt;:&lt;/strong&gt; How to report vulnerabilities&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;PAGI is licensed under the Artistic License 2.0. It's a volunteer project - no timeline commitments, but serious about quality. If your organization needs priority support or custom development, contract work is available.&lt;/p&gt;

&lt;p&gt;Async Perl web development is here. Let's build it together.&lt;/p&gt;

</description>
      <category>webdev</category>
      <category>perl</category>
    </item>
    <item>
      <title>PAGI::Server Performance and Hardening</title>
      <dc:creator>John Napiorkowski</dc:creator>
      <pubDate>Mon, 15 Dec 2025 16:52:22 +0000</pubDate>
      <link>https://dev.to/jjn1056/pagiserver-performance-and-hardening-l8d</link>
      <guid>https://dev.to/jjn1056/pagiserver-performance-and-hardening-l8d</guid>
      <description>&lt;p&gt;Some raw info on how PAGI::Server, the reference implementation of the PAGI spec (&lt;a href="https://github.com/jjn1056/pagi" rel="noopener noreferrer"&gt;https://github.com/jjn1056/pagi&lt;/a&gt;) is coming along performance wise.   Here's some high concurrency testing on a basic 'Hello world' app: &lt;a href="https://github.com/jjn1056/pagi/blob/main/examples/01-hello-http/app.pl" rel="noopener noreferrer"&gt;https://github.com/jjn1056/pagi/blob/main/examples/01-hello-http/app.pl&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;This testing is running off my MacBook Pro, Intel era (2.4 GHz 8-Core Intel Core i9) which is not particularly known for being a great server.   Running as:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt; LIBEV_FLAGS=8 ./bin/pagi-server --workers 16 --quiet --no-access-log --loop EV  ./examples/01-hello-http/app.pl 

&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Results:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;% hey -z 30s -c 500 http://localhost:5000/

Summary:
  Total:    30.0217 secs
  Slowest:  0.1110 secs
  Fastest:  0.0097 secs
  Average:  0.0312 secs
  Requests/sec: 16010.2544


Response time histogram:
  0.010 [1] |
  0.020 [376]   |
  0.030 [222649]    |■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■
  0.040 [226482]    |■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■
  0.050 [26058] |■■■■■
  0.060 [4279]  |■
  0.070 [492]   |
  0.081 [123]   |
  0.091 [173]   |
  0.101 [20]    |
  0.111 [2] |


Latency distribution:
  10% in 0.0249 secs
  25% in 0.0273 secs
  50% in 0.0304 secs
  75% in 0.0340 secs
  90% in 0.0381 secs
  95% in 0.0414 secs
  99% in 0.0505 secs

Details (average, fastest, slowest):
  DNS+dialup:   0.0000 secs, 0.0097 secs, 0.1110 secs
  DNS-lookup:   0.0000 secs, 0.0000 secs, 0.0193 secs
  req write:    0.0000 secs, 0.0000 secs, 0.0091 secs
  resp wait:    0.0311 secs, 0.0096 secs, 0.1110 secs
  resp read:    0.0000 secs, 0.0000 secs, 0.0035 secs

Status code distribution:
  [200] 480655 responses

&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This beats a similar PSGI hello world running under Starman by 30%, but the important bit to note is that PAGI::Server successfully responded to all requests, whereas Starman fell over by 80% at this load on my machine.   That's why you need to run Starman behind an edge server like Nginx; it just can't take the high concurrency.&lt;/p&gt;

&lt;p&gt;I've also been doing http/websockets compliance and security testing on PAGI::Server, working draft is here:&lt;/p&gt;

&lt;p&gt;&lt;a href="https://github.com/jjn1056/pagi/blob/main/lib/PAGI/Server/Compliance.pod" rel="noopener noreferrer"&gt;https://github.com/jjn1056/pagi/blob/main/lib/PAGI/Server/Compliance.pod&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;volunteers who know a lot about flogging servers very welcomed to help me test this.   In production you are still likely to run behind a proxy or edge server like Ngnix but the more robust it is stand alone the better.&lt;/p&gt;

</description>
      <category>testing</category>
      <category>performance</category>
      <category>showdev</category>
      <category>opensource</category>
    </item>
    <item>
      <title>In Defense of Rudolph (and Against State-Sanctioned Reindeer Bullying)</title>
      <dc:creator>John Napiorkowski</dc:creator>
      <pubDate>Sun, 14 Dec 2025 13:28:34 +0000</pubDate>
      <link>https://dev.to/jjn1056/in-defense-of-rudolph-and-against-state-sanctioned-reindeer-bullying-32gp</link>
      <guid>https://dev.to/jjn1056/in-defense-of-rudolph-and-against-state-sanctioned-reindeer-bullying-32gp</guid>
      <description>&lt;p&gt;Every year around Christmas, we are expected—&lt;em&gt;required&lt;/em&gt;, really—to cheerfully sing along to &lt;em&gt;Rudolph the Red-Nosed Reindeer&lt;/em&gt;, a song that is widely regarded as wholesome, heartwarming, and suitable for children. It’s so culturally entrenched that it didn’t just become a hit song—it spawned a beloved stop-motion movie that gets replayed annually as a kind of unquestioned holiday ritual.&lt;/p&gt;

&lt;p&gt;This is a lie.&lt;/p&gt;

&lt;p&gt;If you actually listen to the lyrics, or worse, watch the movie, &lt;em&gt;Rudolph&lt;/em&gt; is not a story about inclusion or kindness. It is a story about &lt;strong&gt;state-sanctioned bullying&lt;/strong&gt;, conditional acceptance, and the moral philosophy that &lt;em&gt;you are only worthy of dignity if you are useful to power&lt;/em&gt;.&lt;/p&gt;

&lt;p&gt;Let’s review the facts.&lt;/p&gt;

&lt;p&gt;Rudolph is born with a mutation. Not a choice. Not a moral failing. A physical difference he did nothing to earn. His nose glows. That’s it. That’s the crime.&lt;/p&gt;

&lt;p&gt;The response?&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;“All of the other reindeer&lt;br&gt;&lt;br&gt;
Used to laugh and call him names&lt;br&gt;&lt;br&gt;
They never let poor Rudolph&lt;br&gt;&lt;br&gt;
Join in any reindeer games”&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;This is not subtle. This is open, communal bullying. Not just teasing—&lt;strong&gt;systemic exclusion&lt;/strong&gt;. There is no adult intervention. No North Pole HR department. No reindeer anti-harassment training. Everyone sees this happening and shrugs.&lt;/p&gt;

&lt;p&gt;Including Santa.&lt;/p&gt;

&lt;p&gt;And this matters, because Santa is not a kindly grandpa in this universe. He is a &lt;strong&gt;dictator-for-life&lt;/strong&gt;, controlling labor, housing, and social standing at the North Pole. He runs a command economy powered by elf labor and reindeer logistics. He absolutely has the authority to shut this down.&lt;/p&gt;

&lt;p&gt;He does not.&lt;/p&gt;

&lt;p&gt;Instead, the message is clear: &lt;em&gt;conform or suffer&lt;/em&gt;.&lt;/p&gt;

&lt;p&gt;Rudolph is ostracized, isolated, and taught that his difference is shameful. The song doesn’t even pretend otherwise. It treats this as normal. Character-building, even.&lt;/p&gt;

&lt;p&gt;But then—&lt;em&gt;miraculously&lt;/em&gt;—a crisis occurs.&lt;/p&gt;

&lt;p&gt;Fog.&lt;/p&gt;

&lt;p&gt;The supply chain is threatened.&lt;/p&gt;

&lt;p&gt;Christmas itself (read: the regime’s legitimacy) is at risk.&lt;/p&gt;

&lt;p&gt;And suddenly—&lt;em&gt;suddenly&lt;/em&gt;—Rudolph’s mutation is no longer disgusting. It’s &lt;strong&gt;strategic&lt;/strong&gt;.&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;“Then one foggy Christmas Eve&lt;br&gt;&lt;br&gt;
Santa came to say…”&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;Note the phrasing. Santa &lt;em&gt;came to say&lt;/em&gt;. Not to apologize. Not to reflect. Not to acknowledge years of cruelty. He simply rebrands Rudolph’s condition as an asset &lt;em&gt;now that it is useful&lt;/em&gt;.&lt;/p&gt;

&lt;p&gt;Rudolph is allowed to participate—not because he deserves dignity as a being, but because his abnormality can be leveraged to achieve &lt;strong&gt;state objectives&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;This is not redemption. This is &lt;strong&gt;instrumentalization&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;And yes, Rudolph gets applause at the end. The same reindeer who mocked him now “love him.” But only after he proves his value.&lt;/p&gt;

&lt;p&gt;It teaches children that:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Bullying is fine until the bullied person turns out to be valuable.&lt;/li&gt;
&lt;li&gt;Authority figures don’t owe you protection—only results.&lt;/li&gt;
&lt;li&gt;Difference is tolerated &lt;em&gt;only&lt;/em&gt; when it serves power.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Rudolph didn’t change. The world didn’t become kinder. The regime just found a way to exploit what it once despised.&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;You don’t get acceptance for being different.&lt;br&gt;&lt;br&gt;
You get acceptance for being &lt;em&gt;useful&lt;/em&gt;.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;If you’re a programmer, this story might feel… familiar.&lt;/p&gt;

&lt;p&gt;A lot of us grew up as the weird kids. Too intense. Too quiet. Too literal. Too obsessed with systems, patterns, or ideas no one else cared about. We missed social cues. We asked annoying questions. We didn’t “join in the reindeer games.”&lt;/p&gt;

&lt;p&gt;We were tolerated at best. Mocked at worst.&lt;/p&gt;

&lt;p&gt;And then, one day, the fog rolled in.&lt;/p&gt;

&lt;p&gt;Suddenly the same traits—hyperfocus, pattern recognition, obsession with correctness, discomfort with ambiguity—were no longer liabilities. They were &lt;strong&gt;marketable&lt;/strong&gt;. Billable. Recruitable.&lt;/p&gt;

&lt;p&gt;The message didn’t change. The &lt;em&gt;context&lt;/em&gt; did.&lt;/p&gt;

&lt;p&gt;You weren’t accepted because the world became kinder. You were accepted because your difference could now be leveraged by companies, platforms, and institutions that once would have happily ignored you.&lt;/p&gt;

&lt;p&gt;That’s not inclusion. That’s conditional mercy.&lt;/p&gt;

&lt;p&gt;This isn’t to say success is bad, or that being valued for your skills is wrong. It’s to say that it’s worth noticing how often dignity arrives &lt;em&gt;after&lt;/em&gt; usefulness—and how rarely it arrives on its own.&lt;/p&gt;

&lt;p&gt;So no, I don’t sing along anymore.&lt;/p&gt;

&lt;p&gt;Because if &lt;em&gt;Rudolph the Red-Nosed Reindeer&lt;/em&gt; is a Christmas story, it’s not about goodwill toward all.&lt;/p&gt;

&lt;p&gt;It’s about how acceptance works in practice.&lt;/p&gt;

&lt;p&gt;And a lot of us deserved better—long before the fog rolled in.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Author’s note:&lt;/strong&gt; This piece was written collaboratively with an AI. I brought the ideas; it helped me shape them — a process I call vibe thinking.&lt;/p&gt;

</description>
      <category>leadership</category>
      <category>mentalhealth</category>
      <category>watercooler</category>
      <category>discuss</category>
    </item>
    <item>
      <title>The Joy of Code in the Age of Vibe Engineering</title>
      <dc:creator>John Napiorkowski</dc:creator>
      <pubDate>Sat, 13 Dec 2025 01:44:23 +0000</pubDate>
      <link>https://dev.to/jjn1056/the-joy-of-code-in-the-age-of-vibe-engineering-49j2</link>
      <guid>https://dev.to/jjn1056/the-joy-of-code-in-the-age-of-vibe-engineering-49j2</guid>
      <description>&lt;p&gt;I’ve always liked programming because I could get the machine to bend to my will.&lt;/p&gt;

&lt;p&gt;Programming has a puzzle-like pleasure to it. Breaking a messy problem down into smaller and smaller pieces. Staring at a system long enough that the shape of the bug finally reveals itself. Digging through hundreds or thousands of files and finding the one thing that’s off. When it works, it gives me a real sense of accomplishment — the same kind some people get from crosswords or jigsaw puzzles.&lt;/p&gt;

&lt;p&gt;A lot of that joy also came from &lt;em&gt;how&lt;/em&gt; I learned to program. I’m entirely self-taught; I never took a programming class in college that I didn’t fail. That doesn’t make me better than anyone else, but it did shape how I think. Learning outside a traditional path forced me to read a lot of other people’s code, to reverse-engineer decisions, and to develop a certain stubborn self-reliance. I think that background gave me edges and perspectives I might not otherwise have had.&lt;/p&gt;

&lt;p&gt;Open source software was central to that journey. It’s how I learned, and contributing back has always mattered to me — not as virtue signaling or résumé building, but as reciprocity. I’ve benefited enormously from OSS over the years, and giving something back still matters more to me than credit ever has. Of all the things I’ve done professionally, that’s what I’m most proud of.&lt;/p&gt;

&lt;p&gt;For a long time I half-jokingly called myself a &lt;em&gt;bare-knuckles programmer&lt;/em&gt;. I’d slog through things out of bloody stubbornness. If I wrote it, I had written every line of it. There was no code in my projects that exceeded my own ability, even if some of it took far longer than it should have when I needed to stretch and expand my ability.&lt;/p&gt;

&lt;p&gt;That relationship to code is changing.&lt;/p&gt;

&lt;h2&gt;
  
  
  From vibe coding to vibe engineering
&lt;/h2&gt;

&lt;p&gt;I don’t love the term &lt;em&gt;vibe coding&lt;/em&gt;, because it suggests you just tell an AI what you want and it spits something out. That’s not what my experience has been like at all.&lt;/p&gt;

&lt;p&gt;What I’m doing feels more like &lt;strong&gt;vibe engineering&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;It’s real work. It’s planning, constraint-setting, and taste. It’s knowing how to ask the right questions, how to push back, and how to recognize when something smells wrong. A lot of the effort goes into building a plan collaboratively, then interrogating the output:&lt;/p&gt;

&lt;p&gt;&lt;em&gt;Why did you do it this way? What does this code actually mean? What assumptions are hiding here?&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;The work hasn’t disappeared — it’s moved up a level.&lt;/p&gt;

&lt;h2&gt;
  
  
  PAGI and the hollow aftertaste
&lt;/h2&gt;

&lt;p&gt;That shift really hit me during my work on PAGI&lt;br&gt;&lt;br&gt;
(&lt;a href="https://github.com/jjn1056/pagi" rel="noopener noreferrer"&gt;https://github.com/jjn1056/pagi&lt;/a&gt;).&lt;/p&gt;

&lt;p&gt;PAGI wasn’t something I casually prompted into existence. I spent well over a year thinking about it: personal research, research with AI, reading and re-reading existing codebases, staring at designs, backing out of approaches that didn’t feel right, and slowly converging on something I believed was worth building. By the time I started writing serious code, I’d already invested a lot of myself into the problem.&lt;/p&gt;

&lt;p&gt;When I did build it, I vibe-engineered essentially the entire project, including some genuinely complex areas around async code and web servers — topics I’m still not an expert in.&lt;/p&gt;

&lt;p&gt;On one level, it was intoxicating. I produced something I’d been thinking about for a long time. I worked in a domain I’d historically avoided. The system works. It’s real and I think it's going to be a valuable addition to the Perl OSS ecosystem.&lt;/p&gt;

&lt;p&gt;And yet the sense of achievement felt just a little hollow.&lt;/p&gt;

&lt;p&gt;Not because it felt dishonest or unearned. More because my &lt;em&gt;relationship&lt;/em&gt; to the code was different. Some of the code in PAGI is code I could not have written from scratch at the time I wrote it. I understand it now — deeply, in fact, because part of the vibe-engineering process for me is reviewing everything and asking detailed, sometimes annoying questions — but it still feels different than having typed every line myself.&lt;/p&gt;

&lt;p&gt;In the past, there was never any code in my projects that was something I “couldn’t have done.” With PAGI, that line was crossed.&lt;/p&gt;

&lt;h2&gt;
  
  
  Authorship, credit, and open source discomfort
&lt;/h2&gt;

&lt;p&gt;That crossing raises uncomfortable questions, especially in open source.&lt;/p&gt;

&lt;p&gt;Is this really my work? How should I respond to praise for the work? Is AI a tool here, a collaborator, or something closer to ghostwriting?&lt;/p&gt;

&lt;p&gt;What makes this tricky is that I genuinely don’t care much about credit. What I care about is contributing something useful back to a community I’ve benefited from enormously. Still, even if the ethics feel sound, there’s an internal discomfort that’s hard to ignore. Praise for cleverness or design lands oddly when you know how much of the implementation was dialogued into existence rather than hand-forged.&lt;/p&gt;

&lt;p&gt;I don’t have clean answers here, and I’m not sure I want to pretend that I do.&lt;/p&gt;

&lt;h2&gt;
  
  
  Mastery, necessity, and selective depth
&lt;/h2&gt;

&lt;p&gt;Part of what complicates all this is realizing that I don’t actually &lt;em&gt;need&lt;/em&gt; to master everything anymore.&lt;/p&gt;

&lt;p&gt;I don’t have a professional need to be an expert in building secure, performant web servers. That’s never been a core job requirement, and it’s unlikely to become one. In that sense, this isn’t fundamentally different from how I use SQL while knowing relatively little about database internals.  And a big part of the reason to write PAGI is to hide a lot of the complexities of asynchronous web programming behind a higher level abstraction so that less knowledgable programmers can actually use it without having to spend a lot of time mastering low level asynchronous primitives.&lt;/p&gt;

&lt;p&gt;And yet, there’s still a part of me that wonders whether I would have been happier mastering those topics the old way — line by line, mistake by mistake.&lt;/p&gt;

&lt;p&gt;That tension is real. Some of it is about craft. Some of it is about identity. And some of it is about timing. I’m in the final stage of my professional career; I’ll likely be letting go of this work sometime in the next ten years or less. In counterpoint I haven’t been this excited about programming since the late 90s — and that excitement sits right next to a quiet grief for what’s changing.&lt;/p&gt;

&lt;h2&gt;
  
  
  When vibe engineering feels like mercy
&lt;/h2&gt;

&lt;p&gt;What convinced me this isn’t a simple “AI good / AI bad” story was a very different experience.&lt;/p&gt;

&lt;p&gt;At work, I had to translate several thousand lines of dense, tech-debt-laden Perl into our newer Go-based system. It was unpleasant work — the kind of job I like the least. Lots of dead zones, historical baggage, and brittle logic.&lt;/p&gt;

&lt;p&gt;Vibe engineering that task didn’t produce any existential angst at all. It produced relief.&lt;/p&gt;

&lt;p&gt;The cost of getting it wrong was lower. The emotional burden was lighter. I didn’t feel like I’d lost anything meaningful by not hand-crafting every line. That contrast made something clear: the discomfort isn’t about AI — it’s about where I derive meaning.&lt;/p&gt;

&lt;h2&gt;
  
  
  Thinking out loud without the paralysis
&lt;/h2&gt;

&lt;p&gt;There’s another gain here that I didn’t fully appreciate at first.&lt;/p&gt;

&lt;p&gt;Like a lot of programmers, I can get stuck in decision paralysis, especially around architecture; a mild form of OCD I imagine.  I’ll spin on tradeoffs, worried about choosing the “wrong” direction. In the past, the cost of being wrong felt high — not just technically, but personally, because it meant committing a lot of my own effort.&lt;/p&gt;

&lt;p&gt;Dialoguing with AI has helped break that cycle.&lt;/p&gt;

&lt;p&gt;Talking things out — even with something that isn’t human — helps me externalize the problem. I can explore options, stress-test ideas, and move forward with less anxiety. Because the personal cost of a wrong turn is lower, I don’t freeze as often. I make decisions knowing I can revise them without paying the same psychological toll.&lt;/p&gt;

&lt;p&gt;That’s a different kind of joy — quieter than pride, but real.&lt;/p&gt;

&lt;h2&gt;
  
  
  Where joy lives now
&lt;/h2&gt;

&lt;p&gt;I don’t know what a “great programmer” is anymore. I’m still proudest of my open source work. What feels most unquestionably &lt;em&gt;mine&lt;/em&gt; now isn’t every line of code, but the problems I choose, the slant I put on solutions, and the insistence on doing the smallest fix that isn’t dumb and aiming for simple, understandable code and interfaces — something I’ve told more than a few coworkers over the years.&lt;/p&gt;

&lt;p&gt;The joy hasn’t disappeared. But it’s changed shape.&lt;/p&gt;

&lt;p&gt;It’s less solitary. Less about proving I can do everything myself. More about judgment, framing, and knowing when something is wrong even if I didn’t write it first.&lt;/p&gt;

&lt;p&gt;So when I ask whether there’s still joy in coding in the age of vibe engineering, I don’t mean it rhetorically. I mean it honestly.&lt;/p&gt;

&lt;p&gt;I think the joy survives — but it asks us to let go of some old certainties about authorship, mastery, and what it means for the machine to bend to our will.&lt;/p&gt;

&lt;p&gt;And I’m still learning how to live with that.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Author’s note:&lt;/strong&gt; This piece was written collaboratively with an AI. I did the thinking; it helped me talk it through; what I'm calling 'vibe thinking'.&lt;/p&gt;

</description>
      <category>ai</category>
      <category>motivation</category>
      <category>programming</category>
      <category>discuss</category>
    </item>
  </channel>
</rss>
