NAME

PAGI::Middleware::XSendfile - Delegate file serving to reverse proxy

SYNOPSIS

use PAGI::Middleware::Builder;

# For Nginx (X-Accel-Redirect)
my $app = builder {
    enable 'XSendfile',
        type    => 'X-Accel-Redirect',
        mapping => '/protected/files/';  # URL prefix for internal location
    $my_app;
};

# For Apache (mod_xsendfile)
my $app = builder {
    enable 'XSendfile',
        type => 'X-Sendfile';
    $my_app;
};

# For Lighttpd
my $app = builder {
    enable 'XSendfile',
        type => 'X-Lighttpd-Send-File';
    $my_app;
};

DESCRIPTION

PAGI::Middleware::XSendfile intercepts file responses and replaces them with a special header that tells the reverse proxy (Nginx, Apache, Lighttpd) to serve the file directly. This is the recommended approach for serving large files in production, as it:

  • Frees up your application worker immediately

  • Uses the reverse proxy's optimized sendfile implementation

  • Supports all the proxy's features (caching, range requests, etc.)

How It Works

When your application sends a file response:

await $send->({
    type => 'http.response.body',
    file => '/var/www/files/large.bin',
});

This middleware intercepts it and instead sends:

# Headers include: X-Accel-Redirect: /protected/files/large.bin
# Body is empty - proxy serves the file

REVERSE PROXY CONFIGURATION

Nginx

Configure an internal location that maps to your files:

location /protected/files/ {
    internal;
    alias /var/www/files/;
}

Then use:

enable 'XSendfile',
    type    => 'X-Accel-Redirect',
    mapping => { '/var/www/files/' => '/protected/files/' };

Apache

Enable mod_xsendfile and allow sending from your directory:

XSendFile On
XSendFilePath /var/www/files

Then use:

enable 'XSendfile', type => 'X-Sendfile';

Lighttpd

Enable mod_fastcgi with x-sendfile option:

fastcgi.server = (
    "/" => ((
        "socket" => "/tmp/app.sock",
        "x-sendfile" => "enable"
    ))
)

Then use:

enable 'XSendfile', type => 'X-Lighttpd-Send-File';

CONFIGURATION

  • type (required)

    The header type to use. One of:

    X-Accel-Redirect     - Nginx
    X-Sendfile           - Apache mod_xsendfile
    X-Lighttpd-Send-File - Lighttpd
  • mapping (for X-Accel-Redirect)

    Path mapping from filesystem paths to Nginx internal URLs. Can be:

    A string prefix (simple case):

    mapping => '/protected/'
    # /var/www/files/foo.txt => /protected/var/www/files/foo.txt

    A hashref for path translation:

    mapping => { '/var/www/files/' => '/protected/' }
    # /var/www/files/foo.txt => /protected/foo.txt
  • variation (optional)

    Additional string appended to Vary header to prevent caching issues.

RANGE REQUESTS / PARTIAL CONTENT

For best results with XSendfile, disable range handling in your app.

When using XSendfile with a reverse proxy, you should disable Range request handling in your file-serving app (PAGI::App::File or PAGI::Middleware::Static) and let the proxy handle Range requests natively:

use PAGI::Middleware::Builder;
use PAGI::App::File;

my $app = builder {
    enable 'XSendfile',
        type    => 'X-Accel-Redirect',
        mapping => { '/var/www/files/' => '/protected/' };

    PAGI::App::File->new(
        root          => '/var/www/files',
        handle_ranges => 0,  # Let nginx handle Range requests
    )->to_app;
};

With handle_ranges => 0:

  • Your app always sends full file paths via X-Sendfile

  • The proxy receives Range headers directly from clients

  • The proxy handles Range requests using its optimized sendfile

This is more efficient than handling ranges in Perl.

Why Partial Responses Bypass XSendfile

If your app does process Range requests (the default behavior), it sends file responses with offset and length. This middleware will pass such responses through unchanged because reverse proxies don't support byte range parameters in X-Sendfile headers:

# This will use X-Sendfile (full file)
await $send->({
    type => 'http.response.body',
    file => '/path/to/file.bin',
});

# This will bypass X-Sendfile (partial content)
await $send->({
    type   => 'http.response.body',
    file   => '/path/to/file.bin',
    offset => 1000,
    length => 500,
});

The recommended approach is to set handle_ranges => 0 so your app never produces partial responses, and let the proxy handle Range requests.

FILEHANDLE SUPPORT

This middleware supports two types of file responses:

File Path (Recommended)

await $send->({
    type => 'http.response.body',
    file => '/path/to/file.bin',
});

This always works - the path is used directly.

Filehandle with path() Method

use IO::File::WithPath;  # or similar
my $fh = IO::File::WithPath->new('/path/to/file.bin', 'r');

await $send->({
    type => 'http.response.body',
    fh   => $fh,
});

For filehandle responses, the middleware will only intercept if the filehandle object has a path() method that returns the filesystem path. This is compatible with:

Plain filehandles without a path() method will be served normally (not via X-Sendfile). If you need X-Sendfile support for filehandles, add a path method to your IO object:

# Simple approach: bless and add path method
sub make_sendfile_fh {
    my ($path) = @_;
    open my $fh, '<', $path or die $!;
    bless $fh, 'My::FH::WithPath';
    return $fh;
}

package My::FH::WithPath;
sub path { ${*{$_[0]}}{path} }  # or store path however you prefer

EXAMPLE

Complete example with Nginx:

# app.pl
use PAGI::Middleware::Builder;
use Future::AsyncAwait;

my $app = builder {
    enable 'XSendfile',
        type    => 'X-Accel-Redirect',
        mapping => { '/var/www/protected/' => '/internal/' };

    async sub {
        my ($scope, $receive, $send) = @_;

        # Authenticate, authorize, etc.
        my $user = authenticate($scope);
        my $file = authorize_download($user, $scope->{path});

        await $send->({
            type    => 'http.response.start',
            status  => 200,
            headers => [
                ['Content-Type', 'application/octet-stream'],
                ['Content-Disposition', 'attachment; filename="file.bin"'],
            ],
        });
        await $send->({
            type => 'http.response.body',
            file => "/var/www/protected/$file",
        });
    };
};

# nginx.conf
location /internal/ {
    internal;
    alias /var/www/protected/;
}

WHY USE THIS?

Direct file serving from your application (even with sendfile) ties up a worker process for the duration of the transfer. With X-Sendfile:

  • Your app worker is freed immediately after sending headers

  • Nginx/Apache handle the file transfer using optimized kernel sendfile

  • The proxy handles Range requests, caching, and connection management

  • Works correctly with slow clients without blocking your app

This is especially important for large files or slow client connections.

SEE ALSO

PAGI::Middleware - Base class for middleware

Plack::Middleware::XSendfile - Similar middleware for PSGI

https://www.nginx.com/resources/wiki/start/topics/examples/xsendfile/ - Nginx X-Accel-Redirect docs

https://tn123.org/mod_xsendfile/ - Apache mod_xsendfile