diff --git a/lib/Net/ACME2/HTTP.pm b/lib/Net/ACME2/HTTP.pm index d2496ae..ef2e984 100644 --- a/lib/Net/ACME2/HTTP.pm +++ b/lib/Net/ACME2/HTTP.pm @@ -235,6 +235,7 @@ sub _xform_http_error { %{ JSON::decode_json( $exc->get('content') ) }, ); }; + my $json_parse_err = $@; if ($acme_error) { die Net::ACME2::X->create( @@ -245,6 +246,16 @@ sub _xform_http_error { }, ); } + + if ($json_parse_err) { + my $content = $exc->get('content'); + + die Net::ACME2::X->create( + 'Generic', + "Failed to decode ACME error ($json_parse_err); HTTP status ${\$exc->get('status')}: $content", + { http => $exc }, + ); + } } die $exc; diff --git a/t/Net-ACME2-HTTP.t b/t/Net-ACME2-HTTP.t index de263e1..63dd1e2 100644 --- a/t/Net-ACME2-HTTP.t +++ b/t/Net-ACME2-HTTP.t @@ -652,8 +652,14 @@ my $acme_key = Net::ACME2::AccountKey->new($_KEY_PEM); ok($err, '_xform_http_error re-throws when DESTROY clobbers $@'); ok( - eval { $err->isa('Net::ACME2::X::HTTP::Protocol') }, - '_xform_http_error preserves exception type (not empty string)', + eval { $err->isa('Net::ACME2::X::Generic') }, + '_xform_http_error wraps unparsable body in Generic exception', + ) or diag("Got: " . (defined $err ? $err : "(undef)")); + + # The original HTTP::Protocol exception is accessible via get('http') + ok( + eval { $err->get('http')->isa('Net::ACME2::X::HTTP::Protocol') }, + '_xform_http_error carries original HTTP::Protocol in "http" property', ) or diag("Got: " . (defined $err ? $err : "(undef)")); } @@ -732,4 +738,115 @@ my $acme_key = Net::ACME2::AccountKey->new($_KEY_PEM); ) or diag("Got: " . (defined $err ? $err : "(undef)")); } +#---------------------------------------------------------------------- +# Test: non-JSON error body includes parse failure in exception +# +# When a server returns a non-JSON error body (e.g., an HTML proxy error), +# JSON::decode_json() fails inside _xform_http_error(). The parse failure +# and raw response content must be surfaced in the exception so the caller +# can diagnose the server problem — not silently swallowed. +#---------------------------------------------------------------------- + +{ + my $mock = MockUA->new(responses => []); + + no warnings 'redefine'; + local *MockUA::request = sub { + my ($self, $method, $url, $args) = @_; + + die Net::ACME2::X->create( + 'HTTP::Protocol', + { + method => 'GET', + url => $url, + status => 502, + reason => 'Bad Gateway', + headers => {}, + content => 'Bad Gateway', + }, + ); + }; + use warnings 'redefine'; + + my $http = Net::ACME2::HTTP->new( + key => $acme_key, + ua => $mock, + ); + + my $err; + eval { + $http->get('https://example.com/directory'); + 1; + } or $err = $@; + + ok($err, 'non-JSON error body throws exception'); + + my $err_str = "$err"; + + like( + $err_str, + qr/Bad Gateway/, + 'exception includes raw response content', + ); + + like( + $err_str, + qr/JSON|decode|parse/i, + 'exception mentions JSON parse failure', + ); +} + +#---------------------------------------------------------------------- +# Test: truncated JSON error body includes parse failure in exception +#---------------------------------------------------------------------- + +{ + my $mock = MockUA->new(responses => []); + + no warnings 'redefine'; + local *MockUA::request = sub { + my ($self, $method, $url, $args) = @_; + + die Net::ACME2::X->create( + 'HTTP::Protocol', + { + method => 'GET', + url => $url, + status => 500, + reason => 'Internal Server Error', + headers => {}, + content => '{"type":"urn:ietf:params:acme:error:serverInternal","deta', + }, + ); + }; + use warnings 'redefine'; + + my $http = Net::ACME2::HTTP->new( + key => $acme_key, + ua => $mock, + ); + + my $err; + eval { + $http->get('https://example.com/directory'); + 1; + } or $err = $@; + + ok($err, 'truncated JSON error body throws exception'); + + my $err_str = "$err"; + + like( + $err_str, + qr/serverInternal/, + 'truncated JSON: exception includes partial response content', + ); + + # Verify the original HTTP exception is accessible + ok( + eval { $err->get('http')->isa('Net::ACME2::X::HTTP::Protocol') }, + 'exception carries original HTTP::Protocol error', + ); +} + done_testing();