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              
3 2     2   145739 use strict;
  2         4  
  2         63  
4 2     2   7 use warnings;
  2         3  
  2         93  
5 2     2   416 use parent 'PAGI::Middleware';
  2         284  
  2         13  
6 2     2   102 use Future::AsyncAwait;
  2         3  
  2         12  
7 2     2   872 use PAGI::Utils::Random qw(secure_random_bytes);
  2         6  
  2         1374  
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   12 my ($self, $config) = @_;
56              
57 6   100     147 $self->{header} = $config->{header} // 'X-Request-ID';
58 6   100     26 $self->{trust_incoming} = $config->{trust_incoming} // 0;
59 6   50     31 $self->{generator} = $config->{generator} // \&_generate_id;
60             }
61              
62             sub _generate_id {
63 105     105   20005 my ($scope) = @_;
64              
65             # Generate a UUID-like ID: timestamp + PID + counter + secure random
66 105         131 my $time = time();
67 105         126 $counter = ($counter + 1) % 0xFFFF;
68 105         215 my $rand_bytes = secure_random_bytes(6);
69 105         840 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 34 my ($self, $app) = @_;
79              
80 4         10 my $header_name = lc($self->{header});
81              
82 5     5   1155 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     2 for my $h (@{$scope->{headers} // []}) {
  1         4  
94 1 50       3 if (lc($h->[0]) eq $header_name) {
95 1         2 $request_id = $h->[1];
96 1         1 last;
97             }
98             }
99             }
100              
101             # Generate new ID if none found
102 5   66     18 $request_id //= $self->{generator}->($scope);
103              
104             # Add request ID to scope
105 5         41 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         436 my $wrapped_send = async sub {
111 10         13 my ($event) = @_;
112 10 100       26 if ($event->{type} eq 'http.response.start') {
113 5         5 push @{$event->{headers}}, [$self->{header}, $request_id];
  5         12  
114             }
115 10         33 await $send->($event);
116 5         41 };
117              
118 5         14 await $app->($modified_scope, $receive, $wrapped_send);
119 4         18 };
120             }
121              
122             1;
123              
124             __END__