From 4f177fc5db91938b2e66ccfe1fbc2fe205f20a48 Mon Sep 17 00:00:00 2001 From: Toddr Bot Date: Mon, 6 Apr 2026 01:28:33 +0000 Subject: [PATCH] fix: throw exception on unparseable HTTP status in Curl backend Replace silent `warn` + continue with a structured exception when the async Curl backend encounters an unparseable HTTP status line. The old code would set reason to empty string and proceed, silently corrupting the response with no indication of failure. In an async pipeline, this is particularly dangerous since the caller cannot catch warnings. Exception propagates through the promise chain's rejection handler, which converts it to a 599/Internal Exception response consistent with the existing error handling pattern. Add comprehensive test suite covering valid responses, unparseable status lines, and edge cases. --- MANIFEST | 1 + lib/Net/ACME2/Curl.pm | 4 +- t/Net-ACME2-Curl.t | 98 +++++++++++++++++++++++++++++++++++++++++++ 3 files changed, 101 insertions(+), 2 deletions(-) create mode 100644 t/Net-ACME2-Curl.t diff --git a/MANIFEST b/MANIFEST index 13324dc..e77ce8f 100644 --- a/MANIFEST +++ b/MANIFEST @@ -58,6 +58,7 @@ t/Net-ACME2-Challenge-dns_account_01.t t/Net-ACME2-Challenge-http_01-Handler.t t/Net-ACME2-Challenge-tls_alpn_01.t t/Net-ACME2-Challenge.t +t/Net-ACME2-Curl.t t/Net-ACME2-EAB.t t/Net-ACME2-Error.t t/Net-ACME2-get-certificate-guard.t diff --git a/lib/Net/ACME2/Curl.pm b/lib/Net/ACME2/Curl.pm index 39d62fe..1edc44c 100644 --- a/lib/Net/ACME2/Curl.pm +++ b/lib/Net/ACME2/Curl.pm @@ -46,6 +46,7 @@ use Net::Curl::Easy (); use Net::ACME2 (); use Net::ACME2::HTTP::Convert (); +use Net::ACME2::X (); use constant _HTTP_TINY_INTERNAL_EXCEPTION_REASON => 'Internal Exception'; @@ -159,8 +160,7 @@ sub _imitate_http_tiny { $reason = $1; } else { - $reason = q<>; - warn "Unparsable first header line: [$line]\n"; + die Net::ACME2::X->create('Generic', "Unparsable first header line: [$line]"); } } } diff --git a/t/Net-ACME2-Curl.t b/t/Net-ACME2-Curl.t new file mode 100644 index 0000000..f0cfb3b --- /dev/null +++ b/t/Net-ACME2-Curl.t @@ -0,0 +1,98 @@ +#!/usr/bin/env perl + +use strict; +use warnings; + +use Test::More; +use Test::Exception; + +# _imitate_http_tiny is not exported, so we call it via full package name. +# Net::ACME2::Curl requires Net::Curl::Easy (optional XS dep) at compile +# time, so skip the entire test when it is not installed. + +use Net::ACME2::X; + +BEGIN { + eval { require Net::ACME2::Curl; 1 } + or plan skip_all => 'Net::Curl::Easy not available'; +} + +# Minimal mock that stands in for a Net::Curl::Easy instance. +{ + package # hide from PAUSE + MockEasy; + + sub new { + my ( $class, %opts ) = @_; + return bless \%opts, $class; + } + + sub getinfo { + my ( $self, $key ) = @_; + + # Only two keys are used: RESPONSE_CODE and EFFECTIVE_URL. + # We distinguish them by value rather than importing constants. + return $self->{response_code} if $key == 0x00200002; # CURLINFO_RESPONSE_CODE + return $self->{effective_url} if $key == 0x00100001; # CURLINFO_EFFECTIVE_URL + die "MockEasy::getinfo: unknown key $key"; + } +} + +# --- happy path: valid status line ---------------------------------------- + +{ + my $easy = MockEasy->new( + response_code => 200, + effective_url => 'https://example.com/acme', + ); + + my $head = "HTTP/1.1 200 OK\r\ncontent-type: application/json\r\n"; + my $body = '{"status":"valid"}'; + + my $resp = Net::ACME2::Curl::_imitate_http_tiny( $easy, $head, $body ); + + is $resp->{status}, 200, 'status from valid response'; + is $resp->{reason}, 'OK', 'reason parsed from status line'; + is $resp->{content}, $body, 'body passed through'; + is $resp->{headers}{'content-type'}, 'application/json', 'header parsed'; +} + +# --- unparseable status line: must throw, not silently continue ------------ + +sub test_unparseable_status_line_throws { + my $easy = MockEasy->new( + response_code => 200, + effective_url => 'https://example.com/acme', + ); + + # A garbage first line that does not match "PROTO STATUS REASON". + my $head = "GARBAGE\r\ncontent-type: text/plain\r\n"; + my $body = 'irrelevant'; + + # Before the fix this would only warn and return a response with an + # empty reason — silently corrupting the parsed result. After the fix + # it must die with a structured exception. + throws_ok { + Net::ACME2::Curl::_imitate_http_tiny( $easy, $head, $body ); + } + qr/[Uu]nparsable.*header/i, + 'unparseable status line throws instead of warning'; +} + +test_unparseable_status_line_throws(); + +# --- empty header block: reason stays undef / does not crash --------------- + +{ + my $easy = MockEasy->new( + response_code => 0, + effective_url => 'https://example.com/acme', + ); + + # Completely empty header — split produces no lines. + my $resp = Net::ACME2::Curl::_imitate_http_tiny( $easy, q<>, 'body' ); + + is $resp->{reason}, undef, 'empty header yields undef reason (no crash)'; +} + +done_testing();