<?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>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>
    <item>
      <title>PAGI: ASGI For Perl, or the Spiritual Successor to Plack</title>
      <dc:creator>John Napiorkowski</dc:creator>
      <pubDate>Tue, 02 Dec 2025 20:13:34 +0000</pubDate>
      <link>https://dev.to/jjn1056/pagi-asgi-for-perl-or-the-spiritual-successor-to-plack-3kb9</link>
      <guid>https://dev.to/jjn1056/pagi-asgi-for-perl-or-the-spiritual-successor-to-plack-3kb9</guid>
      <description>&lt;h1&gt;
  
  
  Introducing PAGI: Async Web Development for Perl
&lt;/h1&gt;

&lt;p&gt;&lt;strong&gt;TL;DR:&lt;/strong&gt; PAGI (Perl Asynchronous Gateway Interface) is a new specification for async Perl web applications, inspired by Python's ASGI. It supports HTTP, WebSockets, and Server-Sent Events natively, and can wrap existing PSGI applications for backward compatibility.&lt;/p&gt;




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

&lt;p&gt;Modern web applications need more than traditional request-response cycles. Real-time features like live notifications, collaborative editing, and streaming data require persistent connections. This means:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;WebSockets&lt;/strong&gt; for bidirectional communication&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Server-Sent Events&lt;/strong&gt; for efficient server push&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Streaming responses&lt;/strong&gt; for large payloads&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Connection lifecycle management&lt;/strong&gt; for resource pooling&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;PSGI, Perl's venerable web server interface, assumes a synchronous world. While frameworks like Mojolicious have built async capabilities on top, there's no shared standard that allows different async frameworks and servers to interoperate.&lt;/p&gt;

&lt;p&gt;PAGI aims to fill that gap.&lt;/p&gt;

&lt;h2&gt;
  
  
  What is PAGI?
&lt;/h2&gt;

&lt;p&gt;PAGI defines a standard interface between async-capable Perl web servers and applications. If you're familiar with Python's ecosystem, think of it as Perl's answer to ASGI.&lt;/p&gt;

&lt;p&gt;A PAGI application is an async coderef with three parameters:&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;AsyncAwait&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;experimental&lt;/span&gt; &lt;span class="p"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;signatures&lt;/span&gt;&lt;span class="p"&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="nf"&gt;app&lt;/span&gt; &lt;span class="p"&gt;($scope, $receive, $send) {&lt;/span&gt;
    &lt;span class="c1"&gt;# $scope   - connection metadata (type, headers, path, etc.)&lt;/span&gt;
    &lt;span class="c1"&gt;# $receive - async coderef to get events from the client&lt;/span&gt;
    &lt;span class="c1"&gt;# $send    - async coderef to send events to the client&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The &lt;code&gt;$scope-&amp;gt;{type}&lt;/code&gt; tells you what kind of connection you're handling:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Type&lt;/th&gt;
&lt;th&gt;Description&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;http&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Standard HTTP request/response&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;websocket&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Persistent WebSocket connection&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;sse&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Server-Sent Events stream&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;lifespan&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Application startup/shutdown lifecycle&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;h2&gt;
  
  
  A Simple HTTP Example
&lt;/h2&gt;

&lt;p&gt;Here's "Hello World" in raw PAGI:&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;AsyncAwait&lt;/span&gt;&lt;span class="p"&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="nf"&gt;app&lt;/span&gt; &lt;span class="p"&gt;($scope, $receive, $send) {&lt;/span&gt;
    &lt;span class="nb"&gt;die&lt;/span&gt; &lt;span class="p"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;Unsupported: &lt;/span&gt;&lt;span class="si"&gt;$scope&lt;/span&gt;&lt;span class="s2"&gt;-&amp;gt;{type}&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;$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;ne&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="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="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;http.response.start&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;200&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="s1"&gt;content-type&lt;/span&gt;&lt;span class="p"&gt;',&lt;/span&gt; &lt;span class="p"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;text/plain&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;$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="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;http.response.body&lt;/span&gt;&lt;span class="p"&gt;',&lt;/span&gt;
        &lt;span class="s"&gt;body&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;Hello from PAGI!&lt;/span&gt;&lt;span class="p"&gt;',&lt;/span&gt;
        &lt;span class="s"&gt;more&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="mi"&gt;0&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;Run 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/
&lt;span class="c"&gt;# =&amp;gt; Hello from PAGI!&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The response is split into &lt;code&gt;http.response.start&lt;/code&gt; (headers) and &lt;code&gt;http.response.body&lt;/code&gt; (content). This separation enables streaming—send multiple body chunks with &lt;code&gt;more =&amp;gt; 1&lt;/code&gt; before the final &lt;code&gt;more =&amp;gt; 0&lt;/code&gt;.&lt;/p&gt;

&lt;h2&gt;
  
  
  WebSocket Support
&lt;/h2&gt;

&lt;p&gt;WebSockets are first-class citizens in PAGI:&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;async&lt;/span&gt; &lt;span class="k"&gt;sub &lt;/span&gt;&lt;span class="nf"&gt;app&lt;/span&gt; &lt;span class="p"&gt;($scope, $receive, $send) {&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;$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;websocket&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;$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="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;websocket.accept&lt;/span&gt;&lt;span class="p"&gt;'&lt;/span&gt; &lt;span class="p"&gt;});&lt;/span&gt;

        &lt;span class="k"&gt;while&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;my&lt;/span&gt; &lt;span class="nv"&gt;$event&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nv"&gt;await&lt;/span&gt; &lt;span class="nv"&gt;$receive&lt;/span&gt;&lt;span class="o"&gt;-&amp;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;$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;websocket.receive&lt;/span&gt;&lt;span class="p"&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;$msg&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;text&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="sr"&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;bytes&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="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;websocket.send&lt;/span&gt;&lt;span class="p"&gt;',&lt;/span&gt;
                    &lt;span class="s"&gt;text&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;Echo: &lt;/span&gt;&lt;span class="si"&gt;$msg&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="k"&gt;elsif&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="ow"&gt;eq&lt;/span&gt; &lt;span class="p"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;websocket.disconnect&lt;/span&gt;&lt;span class="p"&gt;')&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
                &lt;span class="k"&gt;last&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="p"&gt;}&lt;/span&gt;
    &lt;span class="k"&gt;else&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="nb"&gt;die&lt;/span&gt; &lt;span class="p"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;Unsupported: &lt;/span&gt;&lt;span class="si"&gt;$scope&lt;/span&gt;&lt;span class="s2"&gt;-&amp;gt;{type}&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 event loop pattern is consistent across all connection types: await events from &lt;code&gt;$receive&lt;/code&gt;, send responses via &lt;code&gt;$send&lt;/code&gt;.&lt;/p&gt;

&lt;h2&gt;
  
  
  PSGI Compatibility
&lt;/h2&gt;

&lt;p&gt;One of PAGI's key features is backward compatibility with PSGI. The &lt;code&gt;PAGI::App::WrapPSGI&lt;/code&gt; adapter lets you run existing PSGI applications on a PAGI server:&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;PAGI::App::&lt;/span&gt;&lt;span class="nv"&gt;WrapPSGI&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="c1"&gt;# Your existing Catalyst/Dancer/Plack app&lt;/span&gt;
&lt;span class="k"&gt;my&lt;/span&gt; &lt;span class="nv"&gt;$psgi_app&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nv"&gt;MyApp&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="nv"&gt;psgi_app&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;$wrapper&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nn"&gt;PAGI::App::&lt;/span&gt;&lt;span class="nv"&gt;WrapPSGI&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;psgi_app&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="nv"&gt;$psgi_app&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="nv"&gt;$wrapper&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="nv"&gt;to_app&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The wrapper handles all the translation: building &lt;code&gt;%env&lt;/code&gt; from PAGI scope, collecting request bodies, and converting responses back to PAGI events.&lt;/p&gt;

&lt;p&gt;This means you can:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Run legacy applications on a PAGI server&lt;/li&gt;
&lt;li&gt;Add WebSocket endpoints alongside existing routes&lt;/li&gt;
&lt;li&gt;Migrate incrementally from PSGI to PAGI&lt;/li&gt;
&lt;li&gt;Share connection pools between old and new code&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  PAGI::Simple Micro-Framework
&lt;/h2&gt;

&lt;p&gt;For rapid development, PAGI ships with a micro-framework inspired by Express.js:&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;PAGI::&lt;/span&gt;&lt;span class="nv"&gt;Simple&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="nn"&gt;PAGI::&lt;/span&gt;&lt;span class="nv"&gt;Simple&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;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;My API&lt;/span&gt;&lt;span class="p"&gt;');&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="nv"&gt;get&lt;/span&gt;&lt;span class="p"&gt;('&lt;/span&gt;&lt;span class="s1"&gt;/&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;($c) {&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;text&lt;/span&gt;&lt;span class="p"&gt;('&lt;/span&gt;&lt;span class="s1"&gt;Hello, World!&lt;/span&gt;&lt;span class="p"&gt;');&lt;/span&gt;
&lt;span class="p"&gt;});&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="nv"&gt;get&lt;/span&gt;&lt;span class="p"&gt;('&lt;/span&gt;&lt;span class="s1"&gt;/users/:id&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;($c) {&lt;/span&gt;
    &lt;span class="k"&gt;my&lt;/span&gt; &lt;span class="nv"&gt;$id&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;path_params&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;id&lt;/span&gt;&lt;span class="p"&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;json&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;$id&lt;/span&gt; &lt;span class="p"&gt;});&lt;/span&gt;
&lt;span class="p"&gt;});&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="nv"&gt;post&lt;/span&gt;&lt;span class="p"&gt;('&lt;/span&gt;&lt;span class="s1"&gt;/api/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="k"&gt;sub &lt;/span&gt;&lt;span class="p"&gt;($c) {&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="nv"&gt;$c&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="nv"&gt;json_body&lt;/span&gt;&lt;span class="p"&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;json&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt; &lt;span class="s"&gt;received&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="nv"&gt;$data&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;ok&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;$app&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="nv"&gt;to_app&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;WebSockets are equally clean:&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;$app&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="nv"&gt;websocket&lt;/span&gt;&lt;span class="p"&gt;('&lt;/span&gt;&lt;span class="s1"&gt;/chat&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;($ws) {&lt;/span&gt;
    &lt;span class="nv"&gt;$ws&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="nv"&gt;on&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;message&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;($data) {&lt;/span&gt;
        &lt;span class="nv"&gt;$ws&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="nv"&gt;broadcast&lt;/span&gt;&lt;span class="p"&gt;("&lt;/span&gt;&lt;span class="s2"&gt;Someone said: &lt;/span&gt;&lt;span class="si"&gt;$data&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;PAGI::Simple includes:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Express-style routing with path parameters&lt;/li&gt;
&lt;li&gt;JSON request/response helpers&lt;/li&gt;
&lt;li&gt;Session management&lt;/li&gt;
&lt;li&gt;Middleware support (CORS, logging, rate limiting, etc.)&lt;/li&gt;
&lt;li&gt;Static file serving&lt;/li&gt;
&lt;li&gt;WebSocket rooms and broadcasting&lt;/li&gt;
&lt;li&gt;SSE channels with pub/sub&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Current Status
&lt;/h2&gt;

&lt;p&gt;PAGI is currently in &lt;strong&gt;early beta&lt;/strong&gt;. The test suite passes, the examples work, but it hasn't been battle-tested in production.&lt;/p&gt;

&lt;p&gt;What exists today:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Complete PAGI specification&lt;/li&gt;
&lt;li&gt;Reference server implementation (&lt;code&gt;PAGI::Server&lt;/code&gt;)&lt;/li&gt;
&lt;li&gt;PAGI::Simple micro-framework&lt;/li&gt;
&lt;li&gt;13 example applications&lt;/li&gt;
&lt;li&gt;PSGI compatibility layer&lt;/li&gt;
&lt;li&gt;483 passing tests&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;What it needs:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Developers willing to experiment and provide feedback&lt;/li&gt;
&lt;li&gt;Real-world testing&lt;/li&gt;
&lt;li&gt;Framework authors interested in building on PAGI&lt;/li&gt;
&lt;li&gt;Performance profiling and optimization&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;git clone https://github.com/jjn1056/pagi.git
&lt;span class="nb"&gt;cd &lt;/span&gt;pagi
cpanm &lt;span class="nt"&gt;--installdeps&lt;/span&gt; &lt;span class="nb"&gt;.&lt;/span&gt;
prove &lt;span class="nt"&gt;-l&lt;/span&gt; t/

&lt;span class="c"&gt;# Try the examples&lt;/span&gt;
pagi-server &lt;span class="nt"&gt;--app&lt;/span&gt; examples/01-hello-http/app.pl &lt;span class="nt"&gt;--port&lt;/span&gt; 5000
pagi-server &lt;span class="nt"&gt;--app&lt;/span&gt; examples/simple-01-hello/app.pl &lt;span class="nt"&gt;--port&lt;/span&gt; 5000
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  Why This Matters
&lt;/h2&gt;

&lt;p&gt;Perl has excellent async primitives (&lt;code&gt;IO::Async&lt;/code&gt;, &lt;code&gt;Future::AsyncAwait&lt;/code&gt;), but no shared specification for async web applications. Each framework implements its own approach, which limits interoperability.&lt;/p&gt;

&lt;p&gt;PAGI provides that shared foundation. By standardizing on a common interface:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Servers can focus on performance and protocol handling&lt;/li&gt;
&lt;li&gt;Frameworks can focus on developer experience&lt;/li&gt;
&lt;li&gt;Middleware becomes portable across implementations&lt;/li&gt;
&lt;li&gt;The ecosystem can grow together rather than in isolation&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;If you're interested in the future of async Perl web development, I'd love your feedback. Check out the repository, try the examples, and let me know what you think.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Repository:&lt;/strong&gt; &lt;a href="https://github.com/jjn1056/pagi" rel="noopener noreferrer"&gt;github.com/jjn1056/pagi&lt;/a&gt;&lt;/p&gt;




&lt;p&gt;&lt;em&gt;PAGI is not yet on CPAN. It's experimental software—please don't use it in production unless you really know what you're doing.&lt;/em&gt;&lt;/p&gt;

</description>
      <category>perl</category>
      <category>webdev</category>
    </item>
  </channel>
</rss>
