File Coverage

lib/PAGI/Middleware/RequestId.pm
Criterion Covered Total %
statement 47 49 95.9
branch 6 8 75.0
condition 8 11 72.7
subroutine 9 9 100.0
pod 1 1 100.0
total 71 78 91.0


line stmt bran cond sub pod time code
1             package PAGI::Middleware::RequestId;
2             $PAGI::Middleware::RequestId::VERSION = '0.002000';
3 2     2   148040 use strict;
  2         2  
  2         59  
4 2     2   7 use warnings;
  2         3  
  2         80  
5 2     2   376 use parent 'PAGI::Middleware';
  2         231  
  2         10  
6 2     2   137 use Future::AsyncAwait;
  2         2  
  2         9  
7 2     2   850 use PAGI::Utils::Random qw(secure_random_bytes);
  2         4  
  2         1590  
8              
9             =head1 NAME
10              
11             PAGI::Middleware::RequestId - Unique request ID middleware
12              
13             =head1 SYNOPSIS
14              
15             use PAGI::Middleware::Builder;
16              
17             my $app = builder {
18             enable 'RequestId',
19             header => 'X-Request-ID',
20             trust_incoming => 0;
21             $my_app;
22             };
23              
24             =head1 DESCRIPTION
25              
26             PAGI::Middleware::RequestId generates unique request IDs and adds them
27             to both the scope and response headers. This is useful for request
28             tracing and log correlation.
29              
30             =head1 CONFIGURATION
31              
32             =over 4
33              
34             =item * header (default: 'X-Request-ID')
35              
36             The header name to use for the request ID.
37              
38             =item * trust_incoming (default: 0)
39              
40             If true, use an existing request ID from the incoming request headers
41             instead of generating a new one.
42              
43             =item * generator (default: built-in UUID generator)
44              
45             A coderef that generates unique IDs. Receives the scope as argument.
46              
47             =back
48              
49             =cut
50              
51             # Simple counter for uniqueness within process
52             my $counter = 0;
53              
54             sub _init {
55 6     6   94 my ($self, $config) = @_;
56              
57 6   100     37 $self->{header} = $config->{header} // 'X-Request-ID';
58 6   100     22 $self->{trust_incoming} = $config->{trust_incoming} // 0;
59 6   50     27 $self->{generator} = $config->{generator} // \&_generate_id;
60             }
61              
62             sub _generate_id {
63 105     105   19153 my ($scope) = @_;
64              
65             # Generate a UUID-like ID: timestamp + PID + counter + secure random
66 105         126 my $time = time();
67 105         152 $counter = ($counter + 1) % 0xFFFF;
68 105         198 my $rand_bytes = secure_random_bytes(6);
69 105         791 return sprintf('%08x-%04x-%04x-%s',
70             $time,
71             $$ & 0xFFFF,
72             $counter,
73             unpack('H12', $rand_bytes),
74             );
75             }
76              
77             sub wrap {
78 4     4 1 54 my ($self, $app) = @_;
79              
80 4         11 my $header_name = lc($self->{header});
81              
82 5     5   1037 return async sub {
83 5         10 my ($scope, $receive, $send) = @_;
84             # Only handle HTTP requests
85 5 50       13 if ($scope->{type} ne 'http') {
86 0         0 await $app->($scope, $receive, $send);
87 0         0 return;
88             }
89              
90             # Check for existing request ID if trust_incoming is enabled
91 5         5 my $request_id;
92 5 100       12 if ($self->{trust_incoming}) {
93 1   50     1 for my $h (@{$scope->{headers} // []}) {
  1         3  
94 1 50       3 if (lc($h->[0]) eq $header_name) {
95 1         1 $request_id = $h->[1];
96 1         2 last;
97             }
98             }
99             }
100              
101             # Generate new ID if none found
102 5   66     19 $request_id //= $self->{generator}->($scope);
103              
104             # Add request ID to scope
105 5         35 my $modified_scope = $self->modify_scope($scope, {
106             request_id => $request_id,
107             });
108              
109             # Intercept send to add request ID to response
110 10         434 my $wrapped_send = async sub {
111 10         12 my ($event) = @_;
112 10 100       21 if ($event->{type} eq 'http.response.start') {
113 5         6 push @{$event->{headers}}, [$self->{header}, $request_id];
  5         13  
114             }
115 10         27 await $send->($event);
116 5         21 };
117              
118 5         14 await $app->($modified_scope, $receive, $wrapped_send);
119 4         19 };
120             }
121              
122             1;
123              
124             __END__