From 851b4486b12d74237516b3eef5a02f4efb5921c7 Mon Sep 17 00:00:00 2001 From: Samantha Voigt Date: Tue, 16 Jun 2026 12:32:54 -0700 Subject: [PATCH] Resolve $ref in response header objects `Doc::Response#headers` mapped the raw header hash straight to `Doc::Header`, so a header defined as a `$ref` (e.g. to `#/components/headers/*`) was never dereferenced. `Doc::Header#schema` then returned `nil`, and `Validators::Headers` raised `NoMethodError: undefined method 'empty?' for nil` from `JSONSchemer.schema(nil)` whenever such a header was present on the response being validated. Build headers via `@schema.navigate('headers')`, mirroring how `WithParameters#parameters` already resolves parameters, so inline and referenced headers are followed identically. Co-authored-by: Claude --- lib/openapi_contracts/doc/response.rb | 8 +++---- .../openapi/components/headers/TraceId.yaml | 4 ++++ spec/fixtures/openapi/paths/user.yaml | 2 ++ spec/openapi_contracts/doc/response_spec.rb | 19 +++++++++++++++ .../validators/headers_spec.rb | 24 +++++++++++++++++++ 5 files changed, 52 insertions(+), 5 deletions(-) create mode 100644 spec/fixtures/openapi/components/headers/TraceId.yaml diff --git a/lib/openapi_contracts/doc/response.rb b/lib/openapi_contracts/doc/response.rb index 5880830..f1662df 100644 --- a/lib/openapi_contracts/doc/response.rb +++ b/lib/openapi_contracts/doc/response.rb @@ -11,11 +11,9 @@ def initialize(operation, status, schema) end def headers - return @headers if instance_variable_defined? :@headers - - @headers = @schema.fetch('headers', {}).map do |(key, val)| - Doc::Header.new(key, val) - end + @headers ||= Array.wrap( + @schema.navigate('headers')&.each&.map { |key, schema| Doc::Header.new(key, schema.to_h) } + ) end def schema_for(media_type) diff --git a/spec/fixtures/openapi/components/headers/TraceId.yaml b/spec/fixtures/openapi/components/headers/TraceId.yaml new file mode 100644 index 0000000..5f39a41 --- /dev/null +++ b/spec/fixtures/openapi/components/headers/TraceId.yaml @@ -0,0 +1,4 @@ +description: Distributed trace identifier. +schema: + type: string + format: uuid diff --git a/spec/fixtures/openapi/paths/user.yaml b/spec/fixtures/openapi/paths/user.yaml index f249d91..559439b 100644 --- a/spec/fixtures/openapi/paths/user.yaml +++ b/spec/fixtures/openapi/paths/user.yaml @@ -12,6 +12,8 @@ get: schema: type: string required: true + x-trace-id: + $ref: ../components/headers/TraceId.yaml content: application/json: schema: diff --git a/spec/openapi_contracts/doc/response_spec.rb b/spec/openapi_contracts/doc/response_spec.rb index 7efcaa7..d5370bd 100644 --- a/spec/openapi_contracts/doc/response_spec.rb +++ b/spec/openapi_contracts/doc/response_spec.rb @@ -59,5 +59,24 @@ it { is_expected.to be_nil } end end + + describe '#headers' do + context 'when a header is defined inline' do + subject(:header) { response.headers.find { |h| h.name == 'x-request-id' } } + + it 'exposes its schema' do + expect(header.schema).to eq('type' => 'string') + expect(header.required?).to be true + end + end + + context 'when a header is defined via $ref' do + subject(:header) { response.headers.find { |h| h.name == 'x-trace-id' } } + + it 'follows the ref and exposes the referenced schema' do + expect(header.schema).to eq('type' => 'string', 'format' => 'uuid') + end + end + end end end diff --git a/spec/openapi_contracts/validators/headers_spec.rb b/spec/openapi_contracts/validators/headers_spec.rb index 862ff00..a27c81e 100644 --- a/spec/openapi_contracts/validators/headers_spec.rb +++ b/spec/openapi_contracts/validators/headers_spec.rb @@ -46,4 +46,28 @@ ] end end + + context 'when a header is defined via $ref' do + context 'when its value matches the referenced schema' do + before do + response_headers['x-trace-id'] = '550e8400-e29b-41d4-a716-446655440000' + end + + it 'resolves the ref and has no errors' do + expect(subject.call).to be_empty + end + end + + context 'when its value violates the referenced schema' do + before do + response_headers['x-trace-id'] = 1 + end + + it 'returns the error' do + expect(subject.call).to eq [ + 'Header x-trace-id validation error: value at root is not a string (value: 1)' + ] + end + end + end end