diff --git a/README.md b/README.md index 91cb13f0..775ac5dc 100644 --- a/README.md +++ b/README.md @@ -167,7 +167,7 @@ This piece of middleware validates the contents of the response received from up |raise| false | false | Raise an exception on error instead of responding with a generic error body. | |validate_success_only| true | false | Also validate non-2xx responses only. | |ignore_error| false | false | Validate and ignore result even if validation is error. So always return original data. | -|parse_response_by_content_type| true | true | Parse response body to JSON only if Content-Type header is 'application/json'. When false, this always optimistically parses as JSON without checking for Content-Type header. | +|parse_response_by_content_type| true | true | Parse response body to JSON only if Content-Type header is `application/json` or an `application/*+json` media type such as `application/problem+json`. When false, this always optimistically parses as JSON without checking for Content-Type header. | |strict| false | false | Puts the middleware into strict mode, meaning that response code and content type does not defined in the schema will be responded to with a 500 instead of application's status code. | |strict_reference_validation| always false | false | Raises an exception (`OpenAPIParser::MissingReferenceError`) on middleware load if the provided schema file contains unresolvable references (`$ref:"#/something/not/here"`). Not supported on Hyper-schema parser. Defaults to `false` on OpenAPI3 but will default to `true` in next major version. | diff --git a/lib/committee/schema_validator.rb b/lib/committee/schema_validator.rb index de93d00c..7fcc42b7 100644 --- a/lib/committee/schema_validator.rb +++ b/lib/committee/schema_validator.rb @@ -2,11 +2,18 @@ module Committee module SchemaValidator + JSON_MEDIA_TYPE_PATTERN = %r{\Aapplication/(?:.+\+)?json\z}.freeze + class << self def request_media_type(request) Rack::MediaType.type(request.env['CONTENT_TYPE']) end + def json_media_type?(content_type) + normalized_content_type = Rack::MediaType.type(content_type) + normalized_content_type&.match?(JSON_MEDIA_TYPE_PATTERN) || false + end + # @param [String] prefix # @return [Regexp] def build_prefix_regexp(prefix) diff --git a/lib/committee/schema_validator/hyper_schema.rb b/lib/committee/schema_validator/hyper_schema.rb index fa14d64c..f873285e 100644 --- a/lib/committee/schema_validator/hyper_schema.rb +++ b/lib/committee/schema_validator/hyper_schema.rb @@ -31,7 +31,7 @@ def response_validate(status, headers, response, _test_method = false, custom_bo elsif !full_body.empty? parse_to_json = if validator_option.parse_response_by_content_type content_type_key = headers.keys.detect { |k| k.casecmp?('Content-Type') } - headers.fetch(content_type_key, nil)&.start_with?('application/json') + Committee::SchemaValidator.json_media_type?(headers.fetch(content_type_key, nil)) else true end diff --git a/lib/committee/schema_validator/open_api_3.rb b/lib/committee/schema_validator/open_api_3.rb index df213b51..b1483b13 100644 --- a/lib/committee/schema_validator/open_api_3.rb +++ b/lib/committee/schema_validator/open_api_3.rb @@ -28,7 +28,7 @@ def response_validate(status, headers, response, test_method = false, custom_bod parse_to_json = if validator_option.parse_response_by_content_type content_type_key = headers.keys.detect { |k| k.casecmp?('Content-Type') } - headers.fetch(content_type_key, nil)&.start_with?('application/json') + Committee::SchemaValidator.json_media_type?(headers.fetch(content_type_key, nil)) else true end diff --git a/test/middleware/response_validation_open_api_3_test.rb b/test/middleware/response_validation_open_api_3_test.rb index 42bd4ab4..19d959fe 100644 --- a/test/middleware/response_validation_open_api_3_test.rb +++ b/test/middleware/response_validation_open_api_3_test.rb @@ -33,6 +33,13 @@ def app assert_equal 200, last_response.status end + it "passes through a valid response with a +json content-type" do + @app = new_response_rack(JSON.generate(CHARACTERS_RESPONSE), { "Content-Type" => "application/problem+json; charset=utf-8" }, schema: open_api_3_schema, parse_response_by_content_type: true,) + + get "/characters" + assert_equal 200, last_response.status + end + it "passes through a invalid json" do @app = new_response_rack("not_json", {}, schema: open_api_3_schema) diff --git a/test/schema_validator_test.rb b/test/schema_validator_test.rb index 3cd48d36..481e753e 100644 --- a/test/schema_validator_test.rb +++ b/test/schema_validator_test.rb @@ -21,6 +21,39 @@ assert_equal 'multipart/form-data', media_type end + it "detects application/json as a JSON media type" do + assert_equal true, Committee::SchemaValidator.json_media_type?("application/json") + end + + it "detects application/problem+json with parameters as a JSON media type" do + assert_equal true, Committee::SchemaValidator.json_media_type?("application/problem+json; charset=utf-8") + end + + it "detects application/vnd.api+json as a JSON media type" do + assert_equal true, Committee::SchemaValidator.json_media_type?("application/vnd.api+json") + end + + it "does not detect application/x-ndjson as a JSON media type" do + assert_equal false, Committee::SchemaValidator.json_media_type?("application/x-ndjson") + end + + it "does not detect non-JSON content types as JSON media types" do + assert_equal false, Committee::SchemaValidator.json_media_type?("test/csv") + assert_equal false, Committee::SchemaValidator.json_media_type?(nil) + end + + it "parses +json responses in the HyperSchema validator" do + schema = Committee::Drivers::OpenAPI2::Driver.new.parse(open_api_2_data) + validator_option = Committee::SchemaValidator::Option.new({ parse_response_by_content_type: true }, schema, :hyper_schema) + router = Committee::SchemaValidator::HyperSchema::Router.new(schema, validator_option) + request = Rack::Request.new({ "REQUEST_METHOD" => "GET", "PATH_INFO" => "/api/pets", "rack.input" => StringIO.new("") }) + validator = Committee::SchemaValidator::HyperSchema.new(router, request, validator_option) + + validator.link.media_type = "application/vnd.api+json" + + validator.response_validate(200, { "Content-Type" => "application/vnd.api+json" }, [JSON.generate([ValidPet])]) + end + it "builds prefix regexp with a path segment boundary" do regexp = Committee::SchemaValidator.build_prefix_regexp("/v1")