diff --git a/CHANGELOG.md b/CHANGELOG.md index 89bb47dd..419de4f4 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -5,6 +5,11 @@ - Reuse `define_dynamic_method` and `define_maybe_yield` methods in `RageController::API` from `Rage::Internal` by [@numice](https://github.com/numice) (#273). - Add the `form_actions` router configuration (#278). +### Fixed + +- [OpenAPI] Fix SystemStackError in Alba parser with circular associations (#268). +- Rewind `rack.input` when parsing request body (#279). + ## [1.23.0] - 2026-04-15 ### Fixed @@ -29,7 +34,6 @@ ### Fixed -- [OpenAPI] Fix SystemStackError in Alba parser with circular associations. - Only parse request body as multipart if the request is multipart by [p8](https://github.com/p8) (#256). ## [1.22.0] - 2026-03-12 diff --git a/lib/rage/params_parser.rb b/lib/rage/params_parser.rb index 20038dd8..df423f52 100644 --- a/lib/rage/params_parser.rb +++ b/lib/rage/params_parser.rb @@ -14,9 +14,9 @@ def self.prepare(env, url_params) end request_params = if content_type.start_with?("application/json") - json_parse(env["rack.input"].read) + json_parse(env["rack.input"].tap { |io| io.rewind }.read) elsif content_type.start_with?("application/x-www-form-urlencoded") - Iodine::Rack::Utils.parse_urlencoded_nested_query(env["rack.input"].read) + Iodine::Rack::Utils.parse_urlencoded_nested_query(env["rack.input"].tap { |io| io.rewind }.read) elsif content_type.start_with?("multipart/form-data") Iodine::Rack::Utils.parse_multipart(env["rack.input"], content_type) end diff --git a/spec/params_parser_spec.rb b/spec/params_parser_spec.rb index 640c0520..f8a03523 100644 --- a/spec/params_parser_spec.rb +++ b/spec/params_parser_spec.rb @@ -22,7 +22,7 @@ "multipart/form-data; boundary=--aa123" end end - let(:rack_input) { instance_double(StringIO, read: body) } + let(:rack_input) { instance_double(StringIO, read: body, rewind: 0) } let(:env) do { @@ -330,4 +330,52 @@ end end end + + context "when rack.input has been consumed by middleware" do + let(:url_params) { {} } + + context "with json body" do + let(:raw_body) { '{"id":5,"name":"test"}' } + let(:rack_input) { StringIO.new(raw_body) } + let(:env) do + { + "IODINE_HAS_BODY" => true, + "QUERY_STRING" => "", + "CONTENT_TYPE" => "application/json", + "rack.input" => rack_input + } + end + + before do + rack_input.read + allow(JSON).to receive(:parse).with(raw_body, symbolize_names: true).and_return({ id: 5, name: "test" }) + end + + it "can still parse the body after rewind" do + expect(subject).to eq({ id: 5, name: "test" }) + end + end + + context "with urlencoded body" do + let(:raw_body) { "id=15&name=test" } + let(:rack_input) { StringIO.new(raw_body) } + let(:env) do + { + "IODINE_HAS_BODY" => true, + "QUERY_STRING" => "", + "CONTENT_TYPE" => "application/x-www-form-urlencoded", + "rack.input" => rack_input + } + end + + before do + rack_input.read + allow(Iodine::Rack::Utils).to receive(:parse_urlencoded_nested_query).with(raw_body).and_return({ id: "15", name: "test" }) + end + + it "can still parse the body after rewind" do + expect(subject).to eq({ id: "15", name: "test" }) + end + end + end end