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