File Coverage

lib/PAGI/Middleware/Head.pm
Criterion Covered Total %
statement 33 34 97.0
branch 11 12 91.6
condition n/a
subroutine 6 6 100.0
pod 1 1 100.0
total 51 53 96.2


line stmt bran cond sub pod time code
1             package PAGI::Middleware::Head;
2              
3 1     1   216845 use strict;
  1         2  
  1         32  
4 1     1   3 use warnings;
  1         1  
  1         47  
5 1     1   329 use parent 'PAGI::Middleware';
  1         277  
  1         5  
6 1     1   48 use Future::AsyncAwait;
  1         2  
  1         3  
7              
8             =head1 NAME
9              
10             PAGI::Middleware::Head - HEAD request handling middleware
11              
12             =head1 SYNOPSIS
13              
14             use PAGI::Middleware::Builder;
15              
16             my $app = builder {
17             enable 'Head';
18             $my_app;
19             };
20              
21             =head1 DESCRIPTION
22              
23             PAGI::Middleware::Head handles HEAD requests by suppressing the response
24             body while preserving all headers. The inner application runs normally
25             (as if it were a GET request), allowing Content-Length and other headers
26             to be calculated, but the body is not sent to the client.
27              
28             This middleware changes the method from HEAD to GET before passing to the
29             inner app, then suppresses the body in the response.
30              
31             =head1 CONFIGURATION
32              
33             No configuration options.
34              
35             =cut
36              
37             sub wrap {
38 10     10 1 2527 my ($self, $app) = @_;
39              
40 10     10   137 return async sub {
41 10         12 my ($scope, $receive, $send) = @_;
42             # Skip for non-HTTP requests
43 10 100       24 if ($scope->{type} ne 'http') {
44 1         4 await $app->($scope, $receive, $send);
45 1         64 return;
46             }
47              
48             # Only handle HEAD requests
49 9         13 my $is_head = $scope->{method} eq 'HEAD';
50              
51 9 100       14 if (!$is_head) {
52 6         8 await $app->($scope, $receive, $send);
53 6         547 return;
54             }
55              
56             # Change HEAD to GET for inner app
57 3         12 my $modified_scope = $self->modify_scope($scope, { method => 'GET' });
58              
59             # Intercept send to suppress body
60 9         358 my $wrapped_send = async sub {
61 9         23 my ($event) = @_;
62 9         8 my $type = $event->{type};
63              
64 9 100       27 if ($type eq 'http.response.start') {
    100          
    50          
65             # Pass through headers as-is (including Content-Length)
66 3         5 await $send->($event);
67             }
68             elsif ($type eq 'http.response.body') {
69             # Suppress body content but preserve the event structure
70             # Send an empty body with more => 0 to complete the response
71 5 100       11 if (!$event->{more}) {
72 3         8 await $send->({
73             type => 'http.response.body',
74             body => '',
75             more => 0,
76             });
77             }
78             # Otherwise, skip the event entirely (streaming chunks)
79             }
80             elsif ($type eq 'http.response.trailers') {
81             # Skip trailers for HEAD requests
82             }
83             else {
84             # Pass through other events
85 0         0 await $send->($event);
86             }
87 3         10 };
88              
89 3         5 await $app->($modified_scope, $receive, $wrapped_send);
90 10         34 };
91             }
92              
93             1;
94              
95             __END__