From 88f6c25232b8b5ba9ebdc86106b151184daebbe8 Mon Sep 17 00:00:00 2001 From: "Aaron K. Clark" Date: Tue, 19 May 2026 13:43:02 -0500 Subject: [PATCH] docs(openapi): align Error schema with the runtime body shape MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The OpenAPI Error component schema declared an `error: string` field that no controller and no middleware ever emits — the project's no-leak policy (pinned by tests/unit/controller-error-shape.test.js) explicitly forbids echoing raw error detail back to the client. Conversely the global error handler in `app/middleware/error-handler.js` does emit `requestId` when the request reached the request-id middleware (and every server-internal 500 response body carries it for log correlation), but the schema never named the field. Net effect: SDK code-gen consuming the spec built client types with a phantom `error` field while missing the real `requestId` field — both wrong directions of drift at once. Replace the bogus `error` property with the real `requestId` one, keep `message` as the only required field, and pin the new shape with a test in tests/api/openapi.test.js. Co-Authored-By: Claude Opus 4.7 (1M context) --- app/config/openapi.js | 13 ++++++++++++- tests/api/openapi.test.js | 22 ++++++++++++++++++++++ 2 files changed, 34 insertions(+), 1 deletion(-) diff --git a/app/config/openapi.js b/app/config/openapi.js index 12724ed..e836b3e 100644 --- a/app/config/openapi.js +++ b/app/config/openapi.js @@ -27,9 +27,20 @@ const securitySchemes = { const errorResponse = { type: 'object', + // Shape emitted by the global error handler + // (app/middleware/error-handler.js) and every controller's 4xx / + // 5xx exit: a `message` string plus an optional `requestId` for + // log correlation. The `error` field declared here previously + // never appeared at runtime — the handler deliberately suppresses + // raw error detail (see tests/unit/controller-error-shape.test.js + // for the policy) so SDK code-gen that consumed this schema was + // building clients with a field that never landed. properties: { message: { type: 'string' }, - error: { type: 'string' }, + requestId: { + type: 'string', + description: 'UUID correlator (same value as the X-Request-Id response header); only present when the request reached the request-id middleware.', + }, }, required: ['message'], }; diff --git a/tests/api/openapi.test.js b/tests/api/openapi.test.js index 4adb070..8026a01 100644 --- a/tests/api/openapi.test.js +++ b/tests/api/openapi.test.js @@ -70,6 +70,28 @@ describe('OpenAPI spec', () => { expect(schemas.TimeEntry.properties.teStartedAt).toBeDefined(); }); + test('Error component schema matches the runtime shape ({message, requestId?})', async () => { + // Pre-#334 the schema declared a free-form `error: string` field + // that the runtime never emitted (controller-error-shape.test.js + // pins the no-leak policy; the error-handler only ever sends + // `{message, requestId?}`). SDK code-gen consuming the spec was + // building clients that read a non-existent field. The replacement + // pins the true runtime shape: required `message`, optional + // `requestId`. + const res = await request(app).get('/openapi.json'); + const err = res.body.components.schemas.Error; + expect(err.type).toBe('object'); + expect(err.properties.message).toBeDefined(); + expect(err.properties.message.type).toBe('string'); + // requestId IS declared; `error` was dropped. + expect(err.properties.requestId).toBeDefined(); + expect(err.properties.requestId.type).toBe('string'); + expect(err.properties.error).toBeUndefined(); + // message is the only required field — requestId only appears + // when the request reached the request-id middleware. + expect(err.required).toEqual(['message']); + }); + test('POST /v1/customer/bulk 201 declares the {message, count, customers} envelope', async () => { // makeBulkCreate (app/controllers/_bulk-helpers.js) emits // {message, count, }. The spec previously had