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 - Lighttpdmapping (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.txtA hashref for path translation:
mapping => { '/var/www/files/' => '/protected/' } # /var/www/files/foo.txt => /protected/foo.txtvariation (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:
Any blessed filehandle with a
path()method
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