| line |
stmt |
bran |
cond |
sub |
pod |
time |
code |
|
1
|
|
|
|
|
|
|
package PAGI::Test::Response; |
|
2
|
|
|
|
|
|
|
|
|
3
|
12
|
|
|
12
|
|
141320
|
use strict; |
|
|
12
|
|
|
|
|
17
|
|
|
|
12
|
|
|
|
|
375
|
|
|
4
|
12
|
|
|
12
|
|
40
|
use warnings; |
|
|
12
|
|
|
|
|
16
|
|
|
|
12
|
|
|
|
|
462
|
|
|
5
|
12
|
|
|
12
|
|
65
|
use Carp 'croak'; |
|
|
12
|
|
|
|
|
26
|
|
|
|
12
|
|
|
|
|
13884
|
|
|
6
|
|
|
|
|
|
|
|
|
7
|
|
|
|
|
|
|
# Maximum bytes of response body to include in JSON decode error messages |
|
8
|
|
|
|
|
|
|
our $JSON_ERROR_BODY_LIMIT = 1500; |
|
9
|
|
|
|
|
|
|
|
|
10
|
|
|
|
|
|
|
sub new { |
|
11
|
82
|
|
|
82
|
1
|
162202
|
my ($class, %args) = @_; |
|
12
|
|
|
|
|
|
|
return bless { |
|
13
|
|
|
|
|
|
|
status => $args{status} // 200, |
|
14
|
|
|
|
|
|
|
headers => $args{headers} // [], |
|
15
|
|
|
|
|
|
|
body => $args{body} // '', |
|
16
|
|
|
|
|
|
|
exception => $args{exception}, |
|
17
|
82
|
|
50
|
|
|
1426
|
}, $class; |
|
|
|
|
100
|
|
|
|
|
|
|
|
|
100
|
|
|
|
|
|
18
|
|
|
|
|
|
|
} |
|
19
|
|
|
|
|
|
|
|
|
20
|
|
|
|
|
|
|
# Status code |
|
21
|
35
|
|
|
35
|
1
|
214
|
sub status { shift->{status} } |
|
22
|
|
|
|
|
|
|
|
|
23
|
|
|
|
|
|
|
# Raw body bytes |
|
24
|
10
|
|
|
10
|
1
|
500
|
sub content { shift->{body} } |
|
25
|
|
|
|
|
|
|
|
|
26
|
|
|
|
|
|
|
# Decoded text based on Content-Type charset |
|
27
|
|
|
|
|
|
|
sub text { |
|
28
|
35
|
|
|
35
|
1
|
1583
|
my ($self) = @_; |
|
29
|
35
|
|
|
|
|
91
|
my $body = $self->{body}; |
|
30
|
35
|
100
|
66
|
|
|
149
|
return $body unless defined $body && length $body; |
|
31
|
|
|
|
|
|
|
|
|
32
|
|
|
|
|
|
|
# Parse charset from Content-Type header |
|
33
|
34
|
|
100
|
|
|
76
|
my $charset = $self->_extract_charset // 'UTF-8'; |
|
34
|
|
|
|
|
|
|
|
|
35
|
34
|
|
|
|
|
203
|
require Encode; |
|
36
|
34
|
|
|
|
|
225
|
return Encode::decode($charset, $body, Encode::FB_CROAK()); |
|
37
|
|
|
|
|
|
|
} |
|
38
|
|
|
|
|
|
|
|
|
39
|
|
|
|
|
|
|
# Extract charset from Content-Type header |
|
40
|
|
|
|
|
|
|
sub _extract_charset { |
|
41
|
34
|
|
|
34
|
|
47
|
my ($self) = @_; |
|
42
|
34
|
100
|
|
|
|
75
|
my $ct = $self->content_type or return undef; |
|
43
|
|
|
|
|
|
|
|
|
44
|
|
|
|
|
|
|
# Match charset=... (with or without quotes) |
|
45
|
31
|
100
|
|
|
|
139
|
if ($ct =~ /charset\s*=\s*"?([^";,\s]+)"?/i) { |
|
46
|
6
|
|
|
|
|
27
|
return $1; |
|
47
|
|
|
|
|
|
|
} |
|
48
|
25
|
|
|
|
|
69
|
return undef; |
|
49
|
|
|
|
|
|
|
} |
|
50
|
|
|
|
|
|
|
|
|
51
|
|
|
|
|
|
|
# Header lookup (case-insensitive) |
|
52
|
|
|
|
|
|
|
sub header { |
|
53
|
44
|
|
|
44
|
1
|
1179
|
my ($self, $name) = @_; |
|
54
|
44
|
|
|
|
|
72
|
$name = lc($name); |
|
55
|
44
|
|
|
|
|
45
|
for my $pair (@{$self->{headers}}) { |
|
|
44
|
|
|
|
|
80
|
|
|
56
|
44
|
100
|
|
|
|
183
|
return $pair->[1] if lc($pair->[0]) eq $name; |
|
57
|
|
|
|
|
|
|
} |
|
58
|
4
|
|
|
|
|
33
|
return undef; |
|
59
|
|
|
|
|
|
|
} |
|
60
|
|
|
|
|
|
|
|
|
61
|
|
|
|
|
|
|
# All headers as hashref (last value wins for duplicates) |
|
62
|
|
|
|
|
|
|
sub headers { |
|
63
|
0
|
|
|
0
|
1
|
0
|
my ($self) = @_; |
|
64
|
0
|
|
|
|
|
0
|
my %h; |
|
65
|
0
|
|
|
|
|
0
|
for my $pair (@{$self->{headers}}) { |
|
|
0
|
|
|
|
|
0
|
|
|
66
|
0
|
|
|
|
|
0
|
$h{lc($pair->[0])} = $pair->[1]; |
|
67
|
|
|
|
|
|
|
} |
|
68
|
0
|
|
|
|
|
0
|
return \%h; |
|
69
|
|
|
|
|
|
|
} |
|
70
|
|
|
|
|
|
|
|
|
71
|
|
|
|
|
|
|
# Status helpers |
|
72
|
4
|
50
|
|
4
|
1
|
10
|
sub is_success { my $s = shift->status; $s >= 200 && $s < 300 } |
|
|
4
|
|
|
|
|
25
|
|
|
73
|
1
|
50
|
|
1
|
1
|
2
|
sub is_redirect { my $s = shift->status; $s >= 300 && $s < 400 } |
|
|
1
|
|
|
|
|
8
|
|
|
74
|
3
|
|
|
3
|
1
|
33
|
sub is_error { my $s = shift->status; $s >= 400 } |
|
|
3
|
|
|
|
|
9
|
|
|
75
|
|
|
|
|
|
|
|
|
76
|
|
|
|
|
|
|
# Exception from app (if trapped) |
|
77
|
11
|
|
|
11
|
1
|
379
|
sub exception { shift->{exception} } |
|
78
|
|
|
|
|
|
|
|
|
79
|
|
|
|
|
|
|
# Parse body as JSON |
|
80
|
|
|
|
|
|
|
sub json { |
|
81
|
24
|
|
|
24
|
1
|
98
|
my ($self) = @_; |
|
82
|
24
|
|
|
|
|
1286
|
require JSON::MaybeXS; |
|
83
|
|
|
|
|
|
|
|
|
84
|
24
|
|
|
|
|
14906
|
my $body = $self->{body}; |
|
85
|
24
|
|
|
|
|
23
|
my $data = eval { JSON::MaybeXS::decode_json($body) }; |
|
|
24
|
|
|
|
|
166
|
|
|
86
|
|
|
|
|
|
|
|
|
87
|
24
|
100
|
|
|
|
57
|
if (my $err = $@) { |
|
88
|
1
|
|
|
|
|
2
|
my $status = $self->status; |
|
89
|
1
|
|
50
|
|
|
3
|
my $ct = $self->content_type // '(none)'; |
|
90
|
|
|
|
|
|
|
|
|
91
|
|
|
|
|
|
|
# Truncate body preview if too long |
|
92
|
1
|
50
|
|
|
|
3
|
my $preview = defined $body ? $body : '(undef)'; |
|
93
|
1
|
50
|
|
|
|
3
|
if (length($preview) > $JSON_ERROR_BODY_LIMIT) { |
|
94
|
0
|
|
|
|
|
0
|
$preview = substr($preview, 0, $JSON_ERROR_BODY_LIMIT) . "\n... [truncated, " . length($body) . " bytes total]"; |
|
95
|
|
|
|
|
|
|
} |
|
96
|
|
|
|
|
|
|
|
|
97
|
1
|
|
|
|
|
10
|
$err =~ s/\s+at \S+ line \d+\.?\s*$//; # Strip file/line from JSON error |
|
98
|
|
|
|
|
|
|
|
|
99
|
1
|
|
|
|
|
133
|
croak "Response body is not valid JSON (status=$status, content-type=$ct)\n" |
|
100
|
|
|
|
|
|
|
. "Body: $preview\n" |
|
101
|
|
|
|
|
|
|
. "JSON error: $err"; |
|
102
|
|
|
|
|
|
|
} |
|
103
|
|
|
|
|
|
|
|
|
104
|
23
|
|
|
|
|
56
|
return $data; |
|
105
|
|
|
|
|
|
|
} |
|
106
|
|
|
|
|
|
|
|
|
107
|
|
|
|
|
|
|
# Convenience header shortcuts |
|
108
|
37
|
|
|
37
|
1
|
101
|
sub content_type { shift->header('content-type') } |
|
109
|
1
|
|
|
1
|
1
|
3
|
sub content_length { shift->header('content-length') } |
|
110
|
1
|
|
|
1
|
1
|
2
|
sub location { shift->header('location') } |
|
111
|
|
|
|
|
|
|
|
|
112
|
|
|
|
|
|
|
1; |
|
113
|
|
|
|
|
|
|
|
|
114
|
|
|
|
|
|
|
__END__ |