Skip to content

Add runtime response header validation with OpenAPI hot-reload#1804

Merged
pmcelhaney merged 9 commits intomainfrom
copilot/fix-response-type-errors
Apr 10, 2026
Merged

Add runtime response header validation with OpenAPI hot-reload#1804
pmcelhaney merged 9 commits intomainfrom
copilot/fix-response-type-errors

Conversation

Copy link
Copy Markdown
Contributor

Copilot AI commented Apr 9, 2026

Summary

Validates response headers at runtime against the OpenAPI spec and reports errors as response-type-error HTTP headers — one header per error, with the same name repeated for multiple errors. Enabled by default; disable with --no-validate-response. Changes to the OpenAPI spec file are picked up automatically without a server restart.

Manual acceptance tests

  • Start counterfact against a spec with a required response header; call the route without setting it — verify a response-type-error header describes the missing header
  • Return a response header with the wrong type (e.g. "not-a-number" for an integer header) — verify a response-type-error header appears with a descriptive message
  • Return multiple invalid headers — verify multiple response-type-error headers appear (one per error, same header name)
  • Return all required response headers with correct types — verify no response-type-error headers appear
  • Start counterfact with --no-validate-response — verify no response-type-error headers appear even when headers are invalid
  • Routes with no response header schemas in the spec behave unchanged (no error headers added)
  • Edit the local OpenAPI spec file while the server is running — verify subsequent requests use the updated validation rules without restarting

Tasks

  • src/server/response-validator.ts (new) — AJV-based validator: checks required headers are present and header values match their schema types; coerces integer/number/boolean strings before validation; falls back to default response spec when the specific status code isn't defined
  • src/server/registry.ts — Added appendedHeaders?: [string, string][] to CounterfactResponseObject to explicitly represent repeated same-name headers as individual entries
  • src/server/dispatcher.ts — After normalizing the response, runs validateResponse when config.validateResponses !== false; maps each error to a separate ["response-type-error", error] tuple in appendedHeaders — one entry per error, no array bundling under a single key
  • src/server/koa-middleware.ts — Iterates response.appendedHeaders and calls ctx.res.appendHeader once per entry, sending each as a genuinely separate HTTP header
  • src/server/config.ts — Added validateResponses: boolean
  • bin/counterfact.js — Added --no-validate-response flag; wired to validateResponses in the config object
  • src/server/load-openapi-document.ts (new) — Extracted loadOpenApiDocument from app.ts into its own server-side module; re-exported from app.ts for backward compatibility
  • src/server/openapi-watcher.ts (new) — OpenApiWatcher class with watch() / stopWatching() methods; watches a local OpenAPI spec file with chokidar and updates dispatcher.openApiDocument on every change event so validation reflects the latest spec without a restart; skips URL-based specs
  • src/app.ts — Instantiates OpenApiWatcher and calls watch() / stopWatching() alongside the other watchers; no longer contains any inline file-watching logic
  • Teststest/server/response-validator.test.ts (15 cases); dispatcher tests updated to assert on appendedHeaders for required-header missing, wrong type, valid, and disabled scenarios
  • Docsdocs/reference.md and docs/faq.md updated to document the feature and the new flag

Copilot AI linked an issue Apr 9, 2026 that may be closed by this pull request
Copilot AI changed the title [WIP] Fix response type errors at runtime Add runtime response header validation Apr 9, 2026
Copilot AI requested a review from pmcelhaney April 9, 2026 21:13
Copy link
Copy Markdown
Collaborator

@pmcelhaney pmcelhaney left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Make the name of the header "response-type-error". It's okay to have multiple response headers with the same name. Don't append a number.

Copy link
Copy Markdown
Contributor Author

Copilot AI commented Apr 9, 2026

Make the name of the header "response-type-error". It's okay to have multiple response headers with the same name. Don't append a number.

Done in 9b3a17f. Each error is now reported as a separate response-type-error header (no number suffix). Multiple errors produce multiple headers with the same name, which Koa already handles correctly via ctx.set(key, string[]).

Copilot AI requested a review from pmcelhaney April 9, 2026 21:36
Comment thread src/server/dispatcher.ts Outdated
...normalizedResponse,
headers: {
...normalizedResponse.headers,
"response-type-error": validation.errors,
Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

No, one header per error.

@pmcelhaney
Copy link
Copy Markdown
Collaborator

Try again

@pmcelhaney
Copy link
Copy Markdown
Collaborator

@copilot try again

Copy link
Copy Markdown
Contributor Author

Copilot AI commented Apr 9, 2026

@copilot try again

Done in 5e8278c. The dispatcher now uses appendedHeaders: [string, string][] — one ["response-type-error", error] tuple per validation error — instead of bundling them as an array under a single header key. koa-middleware iterates appendedHeaders and calls appendHeader once per entry.

Copy link
Copy Markdown
Collaborator

@pmcelhaney pmcelhaney left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

If the OpenAPI file is changed, the changes should be picked up without a restart.

Copy link
Copy Markdown
Contributor Author

Copilot AI commented Apr 10, 2026

If the OpenAPI file is changed, the changes should be picked up without a restart.

Done in f967106. When the server is running with a local OpenAPI spec file, app.ts now watches that file with chokidar and calls loadOpenApiDocument + updates dispatcher.openApiDocument on every change event — so request/response validation reflects the new spec immediately without a restart. URL-based specs (e.g. https://...) are skipped since chokidar can't watch remote files.

Copilot AI changed the title Add runtime response header validation Add runtime response header validation with OpenAPI hot-reload Apr 10, 2026
Copilot AI requested a review from pmcelhaney April 10, 2026 16:04
Copy link
Copy Markdown
Collaborator

@pmcelhaney pmcelhaney left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Move the file watching code out of app.ts. Put it in a file in the server directory.

Copy link
Copy Markdown
Contributor Author

Copilot AI commented Apr 10, 2026

Move the file watching code out of app.ts. Put it in a file in the server directory.

Done in f68d9cd. The watching logic now lives in two new files in src/server/:

  • load-openapi-document.ts — the loadOpenApiDocument function (re-exported from app.ts for backward compat)
  • openapi-watcher.tsOpenApiWatcher class with watch() / stopWatching() methods

app.ts now just instantiates new OpenApiWatcher(config.openApiPath, dispatcher) and calls watch() / stopWatching() alongside the other watchers.

Copilot AI requested a review from pmcelhaney April 10, 2026 16:25
@pmcelhaney pmcelhaney marked this pull request as ready for review April 10, 2026 17:18
@pmcelhaney pmcelhaney enabled auto-merge April 10, 2026 17:18
@pmcelhaney pmcelhaney added this pull request to the merge queue Apr 10, 2026
Merged via the queue into main with commit 5d09384 Apr 10, 2026
8 of 9 checks passed
@pmcelhaney pmcelhaney deleted the copilot/fix-response-type-errors branch April 10, 2026 17:26
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Response type errors at runtime

2 participants