NAME

Reverse::Proxy - a generic, non-blocking PSGI reverse proxy

VERSION

Version 0.02

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's PATH_INFO and QUERY_STRING are appended.

routes => [ $prefix => $base_url, ... ]

Route by PATH_INFO prefix; 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 undef for 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:

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)