NAME
Reverse::Proxy - a generic, non-blocking PSGI reverse proxy
VERSION
Version 0.05
SYNOPSIS
use Reverse::Proxy;
# forward a whole app to one backend
my $app = Reverse::Proxy->new(
upstream => 'http://127.0.0.1:3000',
)->to_app;
# path-prefix routing to several backends (longest prefix wins)
my $app = Reverse::Proxy->new(
routes => [
'/api' => 'http://api-backend:3000',
'/' => 'http://web-backend:8080',
],
)->to_app;
# dynamic target per request
my $app = Reverse::Proxy->new(
resolver => sub {
my $env = shift;
return $env->{HTTP_HOST} =~ /^admin\./
? 'http://admin:9000' : 'http://public:8080';
},
)->to_app;
Run it under any PSGI server; on Hyperman it forwards without blocking:
plackup -s Hyperman -e 'use Reverse::Proxy; Reverse::Proxy->new(upstream => "http://127.0.0.1:3000")->to_app'
DESCRIPTION
Reverse::Proxy is a PSGI application that forwards each request to an upstream HTTP backend and returns the reply, using Fetch as the client. The whole request path - target resolution, header rebuild, body, dispatch, response mapping, streaming and WebSocket/Upgrade tunnelling - runs in C through Fetch's C ABI.
It runs on any PSGI server. On Hyperman - which advertises psgix.loop and psgi.nonblocking - it forwards with Fetch running on the worker's own event loop and hands the server back a Fetch::Future, so a single worker proxies many concurrent requests without a thread or process each. On other servers it makes a blocking Fetch call per request. Either way it reuses one keep-alive connection pool to the upstream per worker.
Standard proxy behaviour is handled for you: hop-by-hop headers (Connection, Keep-Alive, TE, Trailer, Transfer-Encoding, Upgrade, Proxy-Authenticate, Proxy-Authorization, and anything the client's Connection header names) are stripped in both directions; X-Forwarded-For, X-Forwarded-Proto and X-Forwarded-Host are appended; multi-valued response headers such as Set-Cookie are preserved; and an unreachable or failing upstream yields a 502.
The forwarded request target
PATH_INFO reaches a PSGI application percent-decoded, and the request target is written into a request line that terminates at the first space or CRLF. So the path is re-encoded on the way out: any byte at or below 0x20, 0x7f and above, and %, # and ? go back to %XX.
That is a security boundary, not tidiness. Forwarding the decoded bytes verbatim would let a client whose URL contains %0d%0a end the request line and write a second, complete request onto the upstream connection - request smuggling, past whatever the proxy in front of it enforces. Re-encoding rather than rejecting also means a path that decoded to a space is still forwarded as the path that was asked for.
QUERY_STRING is not decoded by the PSGI spec, so it is forwarded byte for byte, with the same control-byte encoding applied and nothing else.
One thing the encoding cannot restore: a %2F in the client's URL is already a plain / by the time any PSGI application sees it, so an upstream that distinguishes the two cannot be told apart through this or any other PSGI proxy.
CONSTRUCTOR
new(%opts)
Exactly one target selector is required:
upstream=> $base_url-
Forward every request to
$base_url(scheme://host[:port][/prefix]). The request'sPATH_INFOandQUERY_STRINGare appended. routes=> [ $prefix => $base_url, ... ]-
Route by
PATH_INFOprefix; the longest matching prefix wins and is stripped from the forwarded path. A'/'prefix acts as a catch-all. resolver=> sub { my $env = shift; ... }-
Return a base URL (or
undeffor a 404) per request.
Options: preserve_host (default false - when true forward the client Host unchanged, otherwise set Host from the upstream URL), timeout (default 30 seconds), tls_verify (default true, for https upstreams), via (default 'Reverse::Proxy' - the Via header value, undef to omit), pool_size (default 64 - size of the keep-alive connection pool to the upstream; raise it towards your peak concurrency so busy workers reuse connections instead of opening fresh ones), and stream (default false - see "STREAMING").
to_app
Return the PSGI $app coderef.
STREAMING
By default a response is buffered and returned whole. Pass stream => 1 to forward the body chunk-by-chunk instead: the proxy uses Fetch's on_headers to send the status and headers as soon as they arrive, then a psgi.streaming writer to pass each body chunk straight through as Fetch delivers it. Nothing is buffered, so large downloads and endless server-sent-event streams flow with flat memory. On Hyperman this runs on the worker's loop (non-blocking); elsewhere it streams within a blocking request.
my $app = Reverse::Proxy->new(upstream => 'http://sse:9000', stream => 1)->to_app;
WEBSOCKETS AND UPGRADE
Requests carrying an Upgrade header (Connection: Upgrade) - WebSocket and any other protocol upgrade - are tunnelled transparently: the proxy hijacks the client socket (psgix.io), replays the raw Upgrade request to the upstream, relays the upstream's 101 back to the client, then splices bytes both ways until either side closes. Frames, ping/pong, fragmentation and close pass through untouched, so no WebSocket framing is parsed.
This needs a server that provides psgix.io (Hyperman does); elsewhere an Upgrade request gets a 501. The tunnel is blocking for its lifetime and occupies one worker, so run enough workers for your expected concurrent upgrades. Both plaintext (ws:// / http://) and TLS (wss:// / https://) upstreams are supported - a TLS upstream reuses Fetch's own client TLS, honouring tls_verify.
LIMITATIONS
The upgrade tunnel is blocking: it holds one worker for the connection's lifetime. A non-blocking, loop-driven byte splice (so a Hyperman worker is not tied up for the tunnel's duration) is planned.
AUTHOR
LNATION <email@lnation.org>
BUGS
Please report any bugs or feature requests to bug-reverse-proxy at rt.cpan.org, or through the web interface at https://rt.cpan.org/NoAuth/ReportBug.html?Queue=Reverse-Proxy. I will be notified, and then you'll automatically be notified of progress on your bug as I make changes.
SUPPORT
You can find documentation for this module with the perldoc command.
perldoc Reverse::Proxy
You can also look for information at:
RT: CPAN's request tracker (report bugs here)
Search CPAN
LICENSE AND COPYRIGHT
This software is Copyright (c) 2026 by LNATION <email@lnation.org>.
This is free software, licensed under:
The Artistic License 2.0 (GPL Compatible)