From 355afe84021691f9edb2a15f05a0d4cff2577b61 Mon Sep 17 00:00:00 2001 From: ydah Date: Thu, 17 Sep 2026 00:24:54 +0900 Subject: [PATCH 1/2] Validate streaming responses from sent chunks --- .../middleware/response_validation.rb | 20 ++++++++++++--- test/middleware/response_validation_test.rb | 25 +++++++++++++++++++ 2 files changed, 41 insertions(+), 4 deletions(-) diff --git a/lib/committee/middleware/response_validation.rb b/lib/committee/middleware/response_validation.rb index 61e47384..475f5cfd 100644 --- a/lib/committee/middleware/response_validation.rb +++ b/lib/committee/middleware/response_validation.rb @@ -18,13 +18,25 @@ def handle(request) streaming_content_parser = retrieve_streaming_content_parser(headers) if streaming_content_parser + original_response = response + streamed_response = [] + response = Enumerator.new do |yielder| + original_response.each do |chunk| + streamed_response << chunk + yielder << chunk + end + end response = Rack::BodyProxy.new(response) do begin - validate(request, status, headers, response, streaming_content_parser) - rescue => e - handle_exception(e, request.env) + original_response.close if original_response.respond_to?(:close) + ensure + begin + validate(request, status, headers, streamed_response, streaming_content_parser) + rescue => e + handle_exception(e, request.env) - raise e if @raise + raise e if @raise + end end end else diff --git a/test/middleware/response_validation_test.rb b/test/middleware/response_validation_test.rb index d781f3dc..a2884286 100644 --- a/test/middleware/response_validation_test.rb +++ b/test/middleware/response_validation_test.rb @@ -212,6 +212,31 @@ def app get "/events/stream" assert_equal 200, last_response.status end + + it 'validates the chunks sent from a body that cannot be read after close' do + closed = false + body = Object.new + body.define_singleton_method(:each) do |&block| + raise IOError, "body is closed" if closed + + ["hello"].each(&block) + end + body.define_singleton_method(:close) { closed = true } + validated_body = nil + options = { schema: open_api_3_streaming_response_schema, streaming_content_parsers: { 'text/event-stream' => ->(body) { validated_body = body } }, raise: true, } + @app = Rack::Builder.new { + use Committee::Middleware::ResponseValidation, options + run ->(_) { [200, { 'content-type' => 'text/event-stream' }, body] } + } + + status, _headers, response_body = @app.call(Rack::MockRequest.env_for("/events/stream")) + + assert_equal 200, status + assert_equal ["hello"], response_body.each.to_a + response_body.close + assert closed + assert_equal "hello", validated_body + end end describe 'application/x-json-stream; customized streaming event' do From 6aa67ec308be4d787cd7f3689480792b886b23ff Mon Sep 17 00:00:00 2001 From: ydah Date: Thu, 24 Sep 2026 15:42:24 +0900 Subject: [PATCH 2/2] Avoid duplicate buffering of streamed responses --- lib/committee/middleware/response_validation.rb | 2 +- lib/committee/schema_validator/hyper_schema.rb | 9 +++++---- lib/committee/schema_validator/open_api_3.rb | 9 +++++---- 3 files changed, 11 insertions(+), 9 deletions(-) diff --git a/lib/committee/middleware/response_validation.rb b/lib/committee/middleware/response_validation.rb index 475f5cfd..9ec43627 100644 --- a/lib/committee/middleware/response_validation.rb +++ b/lib/committee/middleware/response_validation.rb @@ -19,7 +19,7 @@ def handle(request) if streaming_content_parser original_response = response - streamed_response = [] + streamed_response = +"" response = Enumerator.new do |yielder| original_response.each do |chunk| streamed_response << chunk diff --git a/lib/committee/schema_validator/hyper_schema.rb b/lib/committee/schema_validator/hyper_schema.rb index f873285e..4ec1737c 100644 --- a/lib/committee/schema_validator/hyper_schema.rb +++ b/lib/committee/schema_validator/hyper_schema.rb @@ -21,10 +21,11 @@ def request_validate(request) def response_validate(status, headers, response, _test_method = false, custom_body_parser = nil) return unless link_exist? - full_body = +"" - response.each do |chunk| - full_body << chunk - end + full_body = if response.is_a?(String) + response + else + response.each_with_object(+"") { |chunk, body| body << chunk } + end data = if custom_body_parser custom_body_parser.call(full_body) diff --git a/lib/committee/schema_validator/open_api_3.rb b/lib/committee/schema_validator/open_api_3.rb index ebee996d..76f30762 100644 --- a/lib/committee/schema_validator/open_api_3.rb +++ b/lib/committee/schema_validator/open_api_3.rb @@ -21,10 +21,11 @@ def request_validate(request) end def response_validate(status, headers, response, test_method = false, custom_body_parser = nil) - full_body = +"" - response.each do |chunk| - full_body << chunk - end + full_body = if response.is_a?(String) + response + else + response.each_with_object(+"") { |chunk, body| body << chunk } + end parse_to_json = if validator_option.parse_response_by_content_type content_type_key = headers.keys.detect { |k| k.casecmp?('Content-Type') }