PAGI
view release on metacpan or search on metacpan
lib/PAGI/Response.pm view on Meta::CPAN
Send raw bytes as the response body without any encoding. Use this for
binary data or when you've already encoded the content yourself.
=head2 stream
await $res->stream(async sub ($writer) {
await $writer->write("chunk1");
await $writer->write("chunk2");
await $writer->close();
});
Stream response chunks via callback. The callback receives a writer object
with C<write($chunk)>, C<close()>, and C<bytes_written()> methods.
=head2 writer
my $writer = await $res->writer;
my $writer = await $res->writer(on_close => sub { cleanup() });
my $writer = await $res->writer(on_close => async sub { await cleanup() });
Returns a L<PAGI::Response::Writer> directly, sending headers immediately.
Unlike C<stream()>, the writer is not scoped to a callback â you own it
and must call C<close()> when done.
This is useful when the writer needs to be passed to event handlers,
pub/sub callbacks, timers, or other contexts outside a single function:
async sub live_feed {
my ($self, $ctx) = @_;
my $writer = await $ctx->response
->content_type('text/plain')
->writer(on_close => sub { $bus->unsubscribe($id) });
my $id = $bus->subscribe(async sub ($line) {
await $writer->write("$line\n");
});
await $ctx->receive; # wait for disconnect
await $writer->close;
}
The optional C<on_close> callback is registered before headers are sent,
eliminating any race window with fast client disconnects. Sync and async
callbacks are both supported â see L</on_close> under L</WRITER OBJECT>.
=head2 send_file
await $res->send_file('/path/to/file.pdf');
await $res->send_file('/path/to/file.pdf',
filename => 'download.pdf',
inline => 1,
);
# Partial file (for range requests)
await $res->send_file('/path/to/video.mp4',
offset => 1024, # Start from byte 1024
length => 65536, # Send 64KB
);
Send a file as the response. This method uses the PAGI protocol's C<file>
key for efficient server-side streaming. The file is B<not> read into memory.
For production, use L<PAGI::Middleware::XSendfile> to delegate file serving
to your reverse proxy.
B<Options:>
=over 4
=item * C<filename> - Set Content-Disposition attachment filename
=item * C<inline> - Use Content-Disposition: inline instead of attachment
=item * C<offset> - Start position in bytes (default: 0). For range requests.
=item * C<length> - Number of bytes to send. Defaults to file size minus offset.
=back
B<Range Request Example:>
# Manual range request handling
async sub handle_video ($req, $send) {
my $res = PAGI::Response->new($scope, $send);
my $path = '/videos/movie.mp4';
my $size = -s $path;
my $range = $req->header('Range');
if ($range && $range =~ /bytes=(\d+)-(\d*)/) {
my $start = $1;
my $end = $2 || ($size - 1);
my $length = $end - $start + 1;
return await $res->status(206)
->header('Content-Range' => "bytes $start-$end/$size")
->header('Accept-Ranges' => 'bytes')
->send_file($path, offset => $start, length => $length);
}
return await $res->header('Accept-Ranges' => 'bytes')
->send_file($path);
}
B<Note:> For production file serving with full features (ETag caching,
automatic range request handling, conditional GETs, directory indexes),
use L<PAGI::App::File> instead:
use PAGI::App::File;
my $files = PAGI::App::File->new(root => '/var/www/static');
my $app = $files->to_app;
=head1 EXAMPLES
=head2 Complete Raw PAGI Application
use Future::AsyncAwait;
use PAGI::Request;
use PAGI::Response;
my $app = async sub ($scope, $receive, $send) {
return await handle_lifespan($scope, $receive, $send)
if $scope->{type} eq 'lifespan';
lib/PAGI/Response.pm view on Meta::CPAN
my ($path) = @_;
my ($ext) = $path =~ /(\.[^.]+)$/;
return $MIME_TYPES{lc($ext // '')} // 'application/octet-stream';
}
async sub send_file {
my ($self, $path, %opts) = @_;
croak("File not found: $path") unless -f $path;
croak("Cannot read file: $path") unless -r $path;
# Get file size
my $file_size = -s $path;
# Handle offset and length for range requests
my $offset = $opts{offset} // 0;
my $length = $opts{length};
# Validate offset
croak("offset must be non-negative") if $offset < 0;
croak("offset exceeds file size") if $offset > $file_size;
# Calculate actual length to send
my $max_length = $file_size - $offset;
if (defined $length) {
croak("length must be non-negative") if $length < 0;
$length = $max_length if $length > $max_length;
} else {
$length = $max_length;
}
# Set content-type if not already set
my $has_ct = grep { lc($_->[0]) eq 'content-type' } @{$self->{_headers}};
unless ($has_ct) {
$self->content_type(_mime_type($path));
}
# Set content-length based on actual bytes to send
$self->header('content-length', $length);
# Set content-disposition
my $disposition;
if ($opts{inline}) {
$disposition = 'inline';
} elsif ($opts{filename}) {
# Sanitize filename for header
my $safe_filename = $opts{filename};
$safe_filename =~ s/["\r\n]//g;
$disposition = "attachment; filename=\"$safe_filename\"";
}
$self->header('content-disposition', $disposition) if $disposition;
$self->_mark_sent;
# Send response start
await $self->{send}->({
type => 'http.response.start',
status => $self->status, # uses lazy default of 200
headers => $self->{_headers},
});
# Use PAGI file protocol for efficient server-side streaming
my $body_event = {
type => 'http.response.body',
file => $path,
};
# Add offset/length only if not reading from start or not full file
$body_event->{offset} = $offset if $offset > 0;
$body_event->{length} = $length if $length < $max_length;
await $self->{send}->($body_event);
}
# Writer class for streaming responses
package PAGI::Response::Writer {
use strict;
use warnings;
use Future::AsyncAwait;
use Carp qw(croak);
use Scalar::Util qw(blessed);
sub new {
my ($class, $send, %opts) = @_;
my $self = bless {
send => $send,
bytes_written => 0,
closed => 0,
_on_close => [],
}, $class;
push @{$self->{_on_close}}, $opts{on_close} if $opts{on_close};
return $self;
}
async sub write {
my ($self, $chunk) = @_;
die 'Writer already closed' if $self->{closed};
$self->{bytes_written} += length($chunk // '');
await $self->{send}->({
type => 'http.response.body',
body => $chunk,
more => 1,
});
}
async sub close {
my ($self) = @_;
return if $self->{closed};
$self->{closed} = 1;
await $self->{send}->({
type => 'http.response.body',
body => '',
more => 0,
});
for my $cb (@{$self->{_on_close}}) {
eval {
my $r = $cb->();
if (blessed($r) && $r->isa('Future')) {
await $r;
}
};
if ($@) {
warn "PAGI::Response::Writer on_close callback error: $@";
}
}
# Clear callback array to break any closure-based cycles
$self->{_on_close} = [];
}
sub on_close {
my ($self, $cb) = @_;
push @{$self->{_on_close}}, $cb;
return $self;
}
( run in 0.450 second using v1.01-cache-2.11-cpan-140bd7fdf52 )