From 52677b1f13e45c1158b400b9b9b9de3079ba9703 Mon Sep 17 00:00:00 2001 From: Dimitri Mitropoulos Date: Thu, 20 Aug 2026 16:38:45 -0400 Subject: [PATCH] feat: add allowDefault option to operation-2xx-response The rule treats a `default` response as satisfying the 2xx requirement. `allowDefault: false` turns that off, requiring an explicit 2xx status code. Defaults to `true`, so existing behavior is unchanged. --- .changeset/olive-pugs-repeat.md | 6 +++ docs/@v2/rules/oas/operation-2xx-response.md | 23 ++++++++++ .../__tests__/operation-2xx-response.test.ts | 44 +++++++++++++++++++ .../rules/common/operation-2xx-response.ts | 7 ++- packages/core/src/rules/utils.ts | 4 +- 5 files changed, 82 insertions(+), 2 deletions(-) create mode 100644 .changeset/olive-pugs-repeat.md diff --git a/.changeset/olive-pugs-repeat.md b/.changeset/olive-pugs-repeat.md new file mode 100644 index 0000000000..2d6d75c3df --- /dev/null +++ b/.changeset/olive-pugs-repeat.md @@ -0,0 +1,6 @@ +--- +'@redocly/openapi-core': minor +'@redocly/cli': minor +--- + +Added an `allowDefault` option to the `operation-2xx-response` rule, which controls whether a `default` response satisfies the rule. Defaults to `true`, matching the previous behavior. diff --git a/docs/@v2/rules/oas/operation-2xx-response.md b/docs/@v2/rules/oas/operation-2xx-response.md index a5f3a664b4..037f941d68 100644 --- a/docs/@v2/rules/oas/operation-2xx-response.md +++ b/docs/@v2/rules/oas/operation-2xx-response.md @@ -28,6 +28,7 @@ You can greatly improve the developer and user experience of your APIs by making | ---------------- | ------- | ----------------------------------------------------------------------------------------- | | severity | string | Possible values: `off`, `warn`, `error`. Default `warn` (in `recommended` configuration). | | validateWebhooks | boolean | Determines if responses inside webhooks are validated. Default `false`. | +| allowDefault | boolean | Determines if a `default` response satisfies the rule. Default `true`. | An example configuration: @@ -45,6 +46,28 @@ rules: validateWebhooks: true ``` +By default, a `default` response counts as a successful response. +Set `allowDefault: false` to require an explicit 2xx status code: + +```yaml +rules: + operation-2xx-response: + severity: error + allowDefault: false +``` + +With `allowDefault: false`, the following operation is reported, because `default` describes the responses the operation does not list rather than what a successful call returns: + +```yaml +post: + responses: + default: + $ref: ../components/responses/Problem.yaml +``` + +This matters for code generation: a generator reads the 2xx response to produce the return type of the operation. +A `default`-only operation gives it no success shape to model, so the generated client falls back to an untyped or empty result. + ## Examples Given this configuration: diff --git a/packages/core/src/rules/common/__tests__/operation-2xx-response.test.ts b/packages/core/src/rules/common/__tests__/operation-2xx-response.test.ts index c8475d3eff..c33cc96872 100644 --- a/packages/core/src/rules/common/__tests__/operation-2xx-response.test.ts +++ b/packages/core/src/rules/common/__tests__/operation-2xx-response.test.ts @@ -92,6 +92,50 @@ describe('Oas3 operation-2xx-response', () => { expect(replaceSourceWithRef(results)).toMatchInlineSnapshot(`[]`); }); + it('should report for present default when allowDefault is false', async () => { + const document = parseYamlToDocument( + outdent` + openapi: 3.0.0 + paths: + '/test/': + put: + responses: + default: + description: ok + `, + 'foobar.yaml' + ); + + const results = await lintDocument({ + externalRefResolver: new BaseResolver(), + document, + config: await createConfig({ + rules: { + 'operation-2xx-response': { severity: 'error', allowDefault: false }, + }, + }), + }); + + expect(replaceSourceWithRef(results)).toMatchInlineSnapshot(` + [ + { + "location": [ + { + "pointer": "#/paths/~1test~1/put/responses", + "reportOnKey": true, + "source": "foobar.yaml", + }, + ], + "message": "Operation must have at least one \`2XX\` response.", + "reference": "https://redocly.com/docs/cli/rules/oas/operation-2xx-response", + "ruleId": "operation-2xx-response", + "severity": "error", + "suggest": [], + }, + ] + `); + }); + it('should report even if the responses are null', async () => { const document = parseYamlToDocument( outdent` diff --git a/packages/core/src/rules/common/operation-2xx-response.ts b/packages/core/src/rules/common/operation-2xx-response.ts index f74929544c..82c481bbed 100644 --- a/packages/core/src/rules/common/operation-2xx-response.ts +++ b/packages/core/src/rules/common/operation-2xx-response.ts @@ -2,7 +2,10 @@ import type { Oas3Rule, Oas2Rule } from '../../visitors.js'; import type { UserContext } from '../../walk.js'; import { validateResponseCodes } from '../utils.js'; -export const Operation2xxResponse: Oas3Rule | Oas2Rule = ({ validateWebhooks }) => { +export const Operation2xxResponse: Oas3Rule | Oas2Rule = ({ + validateWebhooks, + allowDefault = true, +}) => { return { Paths: { Responses(responses: Record, { report }: UserContext) { @@ -13,6 +16,7 @@ export const Operation2xxResponse: Oas3Rule | Oas2Rule = ({ validateWebhooks }) codeRange: '2XX', report: report as UserContext['report'], reference: 'https://redocly.com/docs/cli/rules/oas/operation-2xx-response', + allowDefault, }); }, }, @@ -27,6 +31,7 @@ export const Operation2xxResponse: Oas3Rule | Oas2Rule = ({ validateWebhooks }) codeRange: '2XX', report: report as UserContext['report'], reference: 'https://redocly.com/docs/cli/rules/oas/operation-2xx-response', + allowDefault, }); }, }, diff --git a/packages/core/src/rules/utils.ts b/packages/core/src/rules/utils.ts index cf2b0f6a2c..471d0ac5ba 100644 --- a/packages/core/src/rules/utils.ts +++ b/packages/core/src/rules/utils.ts @@ -390,17 +390,19 @@ export function validateResponseCodes({ codeRange, report, reference, + allowDefault = true, }: { responseCodes: string[]; codeRange: string; report: UserContext['report']; reference?: string; + allowDefault?: boolean; }) { const responseCodeRegexp = new RegExp(`^${codeRange[0]}[0-9Xx]{2}$`); const containsNeededCode = responseCodes.some( (code) => - (codeRange === '2XX' && code === 'default') || // It's OK to replace 2xx codes with the default + (allowDefault && codeRange === '2XX' && code === 'default') || // It's OK to replace 2xx codes with the default responseCodeRegexp.test(code) );