PAGI

 view release on metacpan or  search on metacpan

lib/PAGI/Middleware/ContentLength.pm  view on Meta::CPAN


use strict;
use warnings;
use parent 'PAGI::Middleware';
use Future::AsyncAwait;

=head1 NAME

PAGI::Middleware::ContentLength - Auto Content-Length header middleware

=head1 SYNOPSIS

    use PAGI::Middleware::Builder;

    my $app = builder {
        enable 'ContentLength';
        $my_app;
    };

=head1 DESCRIPTION

PAGI::Middleware::ContentLength automatically adds a Content-Length header
to responses that don't already have one. It buffers the response body
to calculate the length, then sends the complete response.

This middleware is useful when the application doesn't know the body
length upfront, but you want to avoid chunked encoding.

=head1 CONFIGURATION

=over 4

=item * auto_chunked (default: 0)

If true, skip adding Content-Length and let chunked encoding be used instead.
This is useful for large responses where buffering would be expensive.

=back

=cut

sub _init {
    my ($self, $config) = @_;

    $self->{auto_chunked} = $config->{auto_chunked} // 0;
}

sub wrap {
    my ($self, $app) = @_;

    return async sub  {
        my ($scope, $receive, $send) = @_;
        # Skip for non-HTTP requests
        if ($scope->{type} ne 'http') {
            await $app->($scope, $receive, $send);
            return;
        }

        my @buffered_events;
        my $has_content_length = 0;
        my $is_streaming = 0;
        my $status;
        my @headers;

        # Create intercepting send to buffer response
        my $wrapped_send = async sub  {
        my ($event) = @_;
            my $type = $event->{type};

            if ($type eq 'http.response.start') {
                $status = $event->{status};
                @headers = @{$event->{headers} // []};

                # Check if Content-Length already present
                for my $h (@headers) {
                    if (lc($h->[0]) eq 'content-length') {
                        $has_content_length = 1;
                        last;
                    }
                    # If Transfer-Encoding is chunked, don't add Content-Length
                    if (lc($h->[0]) eq 'transfer-encoding' && lc($h->[1]) eq 'chunked') {
                        $is_streaming = 1;
                        last;
                    }
                }

                # If already has Content-Length or is streaming, pass through
                if ($has_content_length || $is_streaming || $self->{auto_chunked}) {
                    await $send->($event);
                    return;
                }

                # Buffer the start event to add Content-Length later
                push @buffered_events, $event;
            }
            elsif ($type eq 'http.response.body') {
                # If we're passing through (has Content-Length or streaming)
                if ($has_content_length || $is_streaming || $self->{auto_chunked}) {
                    await $send->($event);
                    return;
                }

                # Check if this is a streaming response (more => 1)
                if ($event->{more}) {
                    $is_streaming = 1;

                    # Flush buffered events and switch to pass-through
                    for my $buffered (@buffered_events) {
                        await $send->($buffered);
                    }
                    @buffered_events = ();
                    await $send->($event);
                    return;
                }

                # Buffer body events
                push @buffered_events, $event;
            }
            else {
                # Pass through other events (trailers, etc.)
                await $send->($event);
            }
        };

        # Run the inner app
        await $app->($scope, $receive, $wrapped_send);

        # If we have buffered events, calculate Content-Length and send
        if (@buffered_events && !$has_content_length && !$is_streaming) {
            # Calculate total body length
            my $body_length = 0;
            for my $event (@buffered_events) {
                if ($event->{type} eq 'http.response.body') {
                    $body_length += length($event->{body} // '');
                }
            }

            # Send start with Content-Length
            for my $event (@buffered_events) {
                if ($event->{type} eq 'http.response.start') {
                    push @{$event->{headers}}, ['content-length', $body_length];
                }
                await $send->($event);
            }
        }
    };
}

1;

__END__

=head1 NOTES

=over 4

=item * For streaming responses (multiple body events with more => 1),
this middleware switches to pass-through mode to avoid buffering.

=item * Responses that already have Content-Length are passed through unchanged.

=item * Responses with Transfer-Encoding: chunked are passed through unchanged.

=item * SSE and WebSocket responses should not use this middleware.

=back

=head1 SEE ALSO

L<PAGI::Middleware> - Base class for middleware

=cut



( run in 0.984 second using v1.01-cache-2.11-cpan-140bd7fdf52 )