| line |
stmt |
bran |
cond |
sub |
pod |
time |
code |
|
1
|
|
|
|
|
|
|
package PAGI::Test::Response; |
|
2
|
|
|
|
|
|
|
$PAGI::Test::Response::VERSION = '0.002000'; |
|
3
|
14
|
|
|
14
|
|
151142
|
use strict; |
|
|
14
|
|
|
|
|
20
|
|
|
|
14
|
|
|
|
|
450
|
|
|
4
|
14
|
|
|
14
|
|
65
|
use warnings; |
|
|
14
|
|
|
|
|
16
|
|
|
|
14
|
|
|
|
|
601
|
|
|
5
|
14
|
|
|
14
|
|
50
|
use Carp 'croak'; |
|
|
14
|
|
|
|
|
17
|
|
|
|
14
|
|
|
|
|
14284
|
|
|
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
|
86
|
|
|
86
|
1
|
161383
|
my ($class, %args) = @_; |
|
12
|
|
|
|
|
|
|
return bless { |
|
13
|
|
|
|
|
|
|
status => $args{status} // 200, |
|
14
|
|
|
|
|
|
|
headers => $args{headers} // [], |
|
15
|
|
|
|
|
|
|
body => $args{body} // '', |
|
16
|
|
|
|
|
|
|
exception => $args{exception}, |
|
17
|
86
|
|
50
|
|
|
1664
|
}, $class; |
|
|
|
|
100
|
|
|
|
|
|
|
|
|
100
|
|
|
|
|
|
18
|
|
|
|
|
|
|
} |
|
19
|
|
|
|
|
|
|
|
|
20
|
|
|
|
|
|
|
# Status code |
|
21
|
36
|
|
|
36
|
1
|
295
|
sub status { shift->{status} } |
|
22
|
|
|
|
|
|
|
|
|
23
|
|
|
|
|
|
|
# Raw body bytes |
|
24
|
12
|
|
|
12
|
1
|
543
|
sub content { shift->{body} } |
|
25
|
|
|
|
|
|
|
|
|
26
|
|
|
|
|
|
|
# Decoded text based on Content-Type charset |
|
27
|
|
|
|
|
|
|
sub text { |
|
28
|
35
|
|
|
35
|
1
|
1598
|
my ($self) = @_; |
|
29
|
35
|
|
|
|
|
54
|
my $body = $self->{body}; |
|
30
|
35
|
100
|
66
|
|
|
143
|
return $body unless defined $body && length $body; |
|
31
|
|
|
|
|
|
|
|
|
32
|
|
|
|
|
|
|
# Parse charset from Content-Type header |
|
33
|
34
|
|
100
|
|
|
100
|
my $charset = $self->_extract_charset // 'UTF-8'; |
|
34
|
|
|
|
|
|
|
|
|
35
|
34
|
|
|
|
|
205
|
require Encode; |
|
36
|
34
|
|
|
|
|
249
|
return Encode::decode($charset, $body, Encode::FB_CROAK()); |
|
37
|
|
|
|
|
|
|
} |
|
38
|
|
|
|
|
|
|
|
|
39
|
|
|
|
|
|
|
# Extract charset from Content-Type header |
|
40
|
|
|
|
|
|
|
sub _extract_charset { |
|
41
|
34
|
|
|
34
|
|
44
|
my ($self) = @_; |
|
42
|
34
|
100
|
|
|
|
6468
|
my $ct = $self->content_type or return undef; |
|
43
|
|
|
|
|
|
|
|
|
44
|
|
|
|
|
|
|
# Match charset=... (with or without quotes) |
|
45
|
31
|
100
|
|
|
|
101
|
if ($ct =~ /charset\s*=\s*"?([^";,\s]+)"?/i) { |
|
46
|
4
|
|
|
|
|
15
|
return $1; |
|
47
|
|
|
|
|
|
|
} |
|
48
|
27
|
|
|
|
|
96
|
return undef; |
|
49
|
|
|
|
|
|
|
} |
|
50
|
|
|
|
|
|
|
|
|
51
|
|
|
|
|
|
|
# Header lookup (case-insensitive) |
|
52
|
|
|
|
|
|
|
sub header { |
|
53
|
43
|
|
|
43
|
1
|
1396
|
my ($self, $name) = @_; |
|
54
|
43
|
|
|
|
|
69
|
$name = lc($name); |
|
55
|
43
|
|
|
|
|
35
|
for my $pair (@{$self->{headers}}) { |
|
|
43
|
|
|
|
|
71
|
|
|
56
|
43
|
100
|
|
|
|
178
|
return $pair->[1] if lc($pair->[0]) eq $name; |
|
57
|
|
|
|
|
|
|
} |
|
58
|
4
|
|
|
|
|
19
|
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
|
9
|
sub is_success { my $s = shift->status; $s >= 200 && $s < 300 } |
|
|
4
|
|
|
|
|
26
|
|
|
73
|
1
|
50
|
|
1
|
1
|
3
|
sub is_redirect { my $s = shift->status; $s >= 300 && $s < 400 } |
|
|
1
|
|
|
|
|
6
|
|
|
74
|
3
|
|
|
3
|
1
|
9
|
sub is_error { my $s = shift->status; $s >= 400 } |
|
|
3
|
|
|
|
|
11
|
|
|
75
|
|
|
|
|
|
|
|
|
76
|
|
|
|
|
|
|
# Exception from app (if trapped) |
|
77
|
11
|
|
|
11
|
1
|
370
|
sub exception { shift->{exception} } |
|
78
|
|
|
|
|
|
|
|
|
79
|
|
|
|
|
|
|
# Parse body as JSON |
|
80
|
|
|
|
|
|
|
sub json { |
|
81
|
23
|
|
|
23
|
1
|
104
|
my ($self) = @_; |
|
82
|
23
|
|
|
|
|
610
|
require JSON::MaybeXS; |
|
83
|
|
|
|
|
|
|
|
|
84
|
23
|
|
|
|
|
6910
|
my $body = $self->{body}; |
|
85
|
23
|
|
|
|
|
33
|
my $data = eval { JSON::MaybeXS::decode_json($body) }; |
|
|
23
|
|
|
|
|
159
|
|
|
86
|
|
|
|
|
|
|
|
|
87
|
23
|
100
|
|
|
|
48
|
if (my $err = $@) { |
|
88
|
1
|
|
|
|
|
3
|
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
|
|
|
|
|
148
|
croak "Response body is not valid JSON (status=$status, content-type=$ct)\n" |
|
100
|
|
|
|
|
|
|
. "Body: $preview\n" |
|
101
|
|
|
|
|
|
|
. "JSON error: $err"; |
|
102
|
|
|
|
|
|
|
} |
|
103
|
|
|
|
|
|
|
|
|
104
|
22
|
|
|
|
|
53
|
return $data; |
|
105
|
|
|
|
|
|
|
} |
|
106
|
|
|
|
|
|
|
|
|
107
|
|
|
|
|
|
|
# Convenience header shortcuts |
|
108
|
36
|
|
|
36
|
1
|
92
|
sub content_type { shift->header('content-type') } |
|
109
|
1
|
|
|
1
|
1
|
2
|
sub content_length { shift->header('content-length') } |
|
110
|
1
|
|
|
1
|
1
|
3
|
sub location { shift->header('location') } |
|
111
|
|
|
|
|
|
|
|
|
112
|
|
|
|
|
|
|
1; |
|
113
|
|
|
|
|
|
|
|
|
114
|
|
|
|
|
|
|
__END__ |