NAME
Reverse::Proxy - a generic, non-blocking PSGI reverse proxy
VERSION
Version 0.01
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.
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)