From fe651f8898c198415943af810fc1ad89a44bf0ca Mon Sep 17 00:00:00 2001 From: David Precious Date: Thu, 14 Sep 2023 20:32:51 +0100 Subject: [PATCH 1/3] Rewind body filehandle before reading JSON If something else had already read from the filehandle, we'd get nothing, and end up throwing an error like: ``` [error] Caught exception in engine "Error Parsing POST 'undef', Error: malformed JSON string, neither tag, array, object, number, string or atom, at character offset 0 (before "(end of string)") at /home/davidp/perl5/lib/perl5/Catalyst.pm line 4092, <$fh> chunk 11." ``` ... that sounds like we got an empty POST or something, but in fact the problem was that a plugin had caused the request body to have already been read, so the filehandle wasn't at the beginning. This seek means that we will read and parse the whole body content as intended, even if something had already read from the filehandle. I think this will also likely solve #183. --- lib/Catalyst.pm | 1 + 1 file changed, 1 insertion(+) diff --git a/lib/Catalyst.pm b/lib/Catalyst.pm index 832c7c40..c8cbc2fc 100644 --- a/lib/Catalyst.pm +++ b/lib/Catalyst.pm @@ -4088,6 +4088,7 @@ sub default_data_handlers { my $slurped; return eval { local $/; + $fh->seek(0,0); # in case it's already been read $slurped = $fh->getline; JSON::MaybeXS::decode_json($slurped); # decode_json does utf8 decoding for us } || Catalyst::Exception->throw(sprintf "Error Parsing POST '%s', Error: %s", (defined($slurped) ? $slurped : 'undef') ,$@); From 6314b081164446a29631545aba6ed3c9a425ca2a Mon Sep 17 00:00:00 2001 From: David Precious Date: Mon, 26 Jan 2026 23:52:25 +0000 Subject: [PATCH 2/3] Only rewind if we have a filehandle If we got a JSON request with an empty body, then we won't have a filehandle to seek. That's not a valid request, but we ought to avoid blowing up. --- lib/Catalyst.pm | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/lib/Catalyst.pm b/lib/Catalyst.pm index c8cbc2fc..33d1dbad 100644 --- a/lib/Catalyst.pm +++ b/lib/Catalyst.pm @@ -4088,8 +4088,10 @@ sub default_data_handlers { my $slurped; return eval { local $/; - $fh->seek(0,0); # in case it's already been read - $slurped = $fh->getline; + if ($fh) { + $fh->seek(0,0); # in case it's already been read + $slurped = $fh->getline; + } JSON::MaybeXS::decode_json($slurped); # decode_json does utf8 decoding for us } || Catalyst::Exception->throw(sprintf "Error Parsing POST '%s', Error: %s", (defined($slurped) ? $slurped : 'undef') ,$@); }, From 69b1f56ab7d68c42b08df43558d3477863d06b2b Mon Sep 17 00:00:00 2001 From: David Precious Date: Mon, 26 Jan 2026 23:58:12 +0000 Subject: [PATCH 3/3] Tests for rewinding filehandle before reading body data For PR #186 - make sure that we can POST body data (both normal HTTP POST and a JSON body), have a hook run before the handler which reads from the body filehandle (simulating e.g. a plugin that looks at the request body before the handler runs) and verify that the handler still gets the data via body_data successfully. --- t/rewind_filehandle.t | 49 +++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 49 insertions(+) create mode 100644 t/rewind_filehandle.t diff --git a/t/rewind_filehandle.t b/t/rewind_filehandle.t new file mode 100644 index 00000000..944f4fb7 --- /dev/null +++ b/t/rewind_filehandle.t @@ -0,0 +1,49 @@ +use warnings; +use strict; +use Test::More; + +# Test case for reported issue when an action consumes JSON but a +# POST sends nothing we get a hard error + +{ + package MyApp::Controller::Root; + $INC{'MyApp/Controller/Root.pm'} = __FILE__; + + use base 'Catalyst::Controller'; + + sub auto :Private { + my ($self, $c) = @_; + my $req_body = $c->req->body; + if ($req_body) { + $c->req->body->getline; + } + + return 1; # continue dispatch + } + + sub foo :Local Args(0) POST Consumes(JSON) { + my ($self, $c) =@_; + # try to access the deserialised JSON param via body_params + my $name = $c->req->body_data->{name}; + $c->res->body("Hi, $name!"); + } + + package MyApp; + use Catalyst; + MyApp->setup; +} + +use HTTP::Request::Common; +use Catalyst::Test 'MyApp'; + +{ + # Send POSTed JSON data to our handler that will read the body filehandle + # via $req->body filehandle then call body_params (see GH PR #186) + ok my $res = request POST 'root/foo', + 'Content-Type' => 'application/json', + 'Content' => '{"name": "Jack", "age": 42}'; + + is $res->content, 'Hi, Jack!'; # don't say that on an aeroplane +} + +done_testing();