Skip to content

fix(validation): guard response validation against malformed content and response specs#257

Merged
wadakatu merged 4 commits into
mainfrom
fix/response-malformed-schema-guard-issue-256
May 18, 2026
Merged

fix(validation): guard response validation against malformed content and response specs#257
wadakatu merged 4 commits into
mainfrom
fix/response-malformed-schema-guard-issue-256

Conversation

@wadakatu
Copy link
Copy Markdown
Collaborator

@wadakatu wadakatu commented May 18, 2026

Summary

ResponseBodyValidator and OpenApiResponseValidator lacked the
malformed-schema guards that RequestBodyValidator already had, so the two
sides of the contract behaved asymmetrically on a broken spec. This adds
symmetric guards at every level of the response spec so a non-array
responses[$status] entry, a non-array content block, a non-array
content[mediaType] entry, or a non-array schema is surfaced as a loud
spec-level error.

Why

Fixes #256. Fixes #258.

With no guard, a malformed responses map produced one of two bad outcomes on
the response side:

  • Uncaught TypeError — a non-array responses[$status] entry, a non-array
    content block, or a non-array schema on a JSON media type reached an
    array-typed parameter (validateBody() / validateHeaders() /
    ResponseBodyValidator::validate()) or OpenApiSchemaConverter::convert().
    TypeError extends Error, not RuntimeException, so validateBody()'s
    catch never saw it — the run crashed with a stack trace instead of a readable
    spec error.
  • Diagnostic-free silent pass — a non-JSON media type's schema: null was
    read as "no schema" by the isset(...['schema']) skip check (issue tech-debt(validation) — spec が宣言した非 JSON Content-Type のボディが診断なしで素通りする #254) and
    slipped through as a clean success. The request side rejects the same
    schema: null loudly, so request and response disagreed.

Both contradict the contract-testing principle of surfacing malformed specs
rather than masking them, and broke parity with RequestBodyValidator's guards.

#258 (the non-array responses[$status] entry) was originally split out as a
follow-up; it is the same logical change one level up, so it is folded into this
PR.

Verification

Failing tests were added first (each reproducing a TypeError or a silent
pass), then the guards were implemented. array_key_exists (not isset) is used
so an explicit schema: null is also flagged.

A multi-agent review (/pr-review-toolkit:review-pr) prompted additional tests:
orchestrator-level tests for the malformed media-type-entry and malformed/null
schema guards (for parity with the request-side OpenApiRequestValidatorTest),
a non-JSON non-null scalar schema case, and a test pinning that the guard
pre-scans every media-type entry regardless of content negotiation.

  • composer test passes (1815 tests)
  • composer stan passes
  • composer cs-check passes

Notes for reviewers

  • The guards live at the level each malformed shape first becomes reachable: the
    responses[$status] and content-block guards in
    OpenApiResponseValidator::validate() / validateBody() (their array-typed
    parameters are the crash site), and the per-entry / per-schema guards in
    ResponseBodyValidator::validate(), mirroring RequestBodyValidator.
  • The responses[...] status in the error message uses the matched spec key for
    the responses[$status] guard (a literal spec pointer) and the wire status
    code for the content-level guards (consistent with the validator's existing
    (status N) messages). When the matched key is a range (5XX) or default,
    the wire-status form is not a literal pointer but is enough to locate the
    malformed content.
  • Out of scope and tracked as tech-debt(validation) — OpenApiResponseValidator の spec traversal が非配列の構造ノードで未捕捉 TypeError になる #259: a malformed (non-array) structural node
    above responses[$status] — a non-array paths, path item, operation, or
    responses map — still raises an uncaught TypeError in the spec-traversal
    path of validate(). Hardening that traversal is a separate concern from the
    per-response malformed guards this PR adds.

…t schemas (#256)

ResponseBodyValidator lacked the malformed-schema guards that
RequestBodyValidator already had, so the two sides of the contract
behaved asymmetrically on a broken spec.

Previously, a non-array content[mediaType] entry or a non-array
`schema` could either trigger a confusing TypeError deep in the
converter, or — when `schema` was null — slip through as a silent
pass that validated nothing.

This adds symmetric guards so both cases are surfaced as a loud
spec-level error. OpenApiResponseValidator::validateBody() also now
guards a non-array `content` block before delegating. TDD tests and a
fixture cover both the TypeError and silent-pass regressions.
wadakatu added 2 commits May 18, 2026 19:27
…#256)

Add review-feedback tests for the malformed-schema guards introduced in
the previous commit. No production code change.

- Add orchestrator-level OpenApiResponseValidator tests for the malformed
  media-type entry and malformed/null schema guards, for parity with the
  request-side OpenApiRequestValidatorTest.
- Add a non-JSON non-null scalar schema case.
- Pin that the guard pre-scans every media-type entry regardless of
  content negotiation.

Adds 3 fixture paths to malformed.json and 5 test methods total.
When a responses[$status] spec entry is a non-array scalar (e.g. an
unresolved $ref), the scalar reached the `array $responseSpec`
parameter of validateBody()/validateHeaders() and raised an uncaught
TypeError. Since TypeError extends Error rather than RuntimeException,
it bypassed the validator's error handling and surfaced as a hard crash
instead of a validation result.

validate() now detects this case and returns a loud
`Malformed 'responses[...]'` spec error via
OpenApiValidationResult::failure(), mirroring the content-level guards
already added in this PR and RequestBodyValidator's `requestBody`
guard. A failing test that reproduced the TypeError was added first,
then the guard.

Closes #258
@wadakatu wadakatu changed the title fix(validation): guard ResponseBodyValidator against malformed content schemas (#256) fix(validation): guard response validation against malformed content and response specs May 18, 2026
…us guard (#258)

Add malformed_response_status_entry_keys_message_off_matched_spec_key,
which exercises the wire-status-vs-spec-key dimension the prior test
missed: a spec that declares only a responses[default] entry, hit by a
wire status that resolves to default. This confirms the responses[$status]
guard's error message names the matched spec key rather than the raw wire
status code.

A new /response-default-status-scalar fixture path in malformed.json
backs the test. The OpenApiResponseValidator comment rewording and the
test docblock tweak are non-functional, clarifying that the (issue #258)
tag scopes the new guard.
@wadakatu wadakatu merged commit 18b0a98 into main May 18, 2026
17 checks passed
@wadakatu wadakatu deleted the fix/response-malformed-schema-guard-issue-256 branch May 18, 2026 10:58
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

1 participant