diff --git a/PRE_RELEASE_CHANGELOG.md b/PRE_RELEASE_CHANGELOG.md index b63a2f92..23b73f4d 100644 --- a/PRE_RELEASE_CHANGELOG.md +++ b/PRE_RELEASE_CHANGELOG.md @@ -9,3 +9,8 @@ ## Bug Fixes - **Types:** Add the missing readonly `name: 'CanceledError'` declaration to CommonJS `CanceledError` typings to match the ESM declarations. (**#10922**) +- **Config Merge:** Added `transitional.validateStatusUndefinedResolves` (default `true`) so applications can opt into treating explicit `validateStatus: undefined` like an omitted option by setting it to `false`. `validateStatus: null` still accepts every response status. (**#10899**, closes **#6688**) + +## Release Tracking + +- ESM/CJS typings are updated for `transitional.validateStatusUndefinedResolves`; README/docs updates are tracked in `PRE_RELEASE_DOCS.md` for release preparation. diff --git a/PRE_RELEASE_DOCS.md b/PRE_RELEASE_DOCS.md index d1fda911..329f646d 100644 --- a/PRE_RELEASE_DOCS.md +++ b/PRE_RELEASE_DOCS.md @@ -37,3 +37,23 @@ axios.get('https://api.example.com/users', { ``` - **Notes:** Add a security page row linking to the request-config section and add a `sensitiveHeaders` request-config entry marked Node.js only. + +### validateStatus undefined transitional option + +- **Change:** Document `transitional.validateStatusUndefinedResolves` for the `validateStatus: undefined` merge behavior. +- **Source:** `PRE_RELEASE_CHANGELOG.md` Bug Fixes, #10899, closes #6688. +- **Status:** Pending. +- **Docs targets:** README request config section; `docs/pages/advanced/request-config.md` `validateStatus` section and request config example; translated request-config docs after English docs are finalized. +- **Required content:** Explain that `validateStatus: undefined` keeps legacy behavior by default and resolves every response status because `transitional.validateStatusUndefinedResolves` defaults to `true`. Explain that setting `transitional.validateStatusUndefinedResolves` to `false` makes explicit `validateStatus: undefined` behave like the option was omitted, so axios uses the configured/default validator and rejects non-2xx responses by default. Mention that `validateStatus: null` still accepts every response status, and users who disable the transitional behavior should use `null` or `() => true` when they intentionally want all statuses to resolve. +- **Examples:** Include a short opt-in example. + +```js +axios.get('/user/12345', { + validateStatus: undefined, + transitional: { + validateStatusUndefinedResolves: false + } +}); +``` + +- **Notes:** This is release-prep documentation only; do not update README or docs pages in the feature/fix PR. diff --git a/index.d.cts b/index.d.cts index 3288c0e7..2996ce14 100644 --- a/index.d.cts +++ b/index.d.cts @@ -397,6 +397,7 @@ declare namespace axios { clarifyTimeoutError?: boolean; legacyInterceptorReqResOrdering?: boolean; advertiseZstdAcceptEncoding?: boolean; + validateStatusUndefinedResolves?: boolean; } interface GenericAbortSignal { diff --git a/index.d.ts b/index.d.ts index 31f05e9f..e9392b41 100644 --- a/index.d.ts +++ b/index.d.ts @@ -284,6 +284,7 @@ export interface TransitionalOptions { clarifyTimeoutError?: boolean; legacyInterceptorReqResOrdering?: boolean; advertiseZstdAcceptEncoding?: boolean; + validateStatusUndefinedResolves?: boolean; } export interface GenericAbortSignal { diff --git a/lib/core/Axios.js b/lib/core/Axios.js index 3f71e9a6..158de808 100644 --- a/lib/core/Axios.js +++ b/lib/core/Axios.js @@ -102,6 +102,7 @@ class Axios { clarifyTimeoutError: validators.transitional(validators.boolean), legacyInterceptorReqResOrdering: validators.transitional(validators.boolean), advertiseZstdAcceptEncoding: validators.transitional(validators.boolean), + validateStatusUndefinedResolves: validators.transitional(validators.boolean), }, false ); diff --git a/lib/core/mergeConfig.js b/lib/core/mergeConfig.js index 760f5ad7..a31a9f16 100644 --- a/lib/core/mergeConfig.js +++ b/lib/core/mergeConfig.js @@ -68,6 +68,28 @@ export default function mergeConfig(config1, config2) { } } + function getMergedTransitionalOption(prop) { + const transitional2 = utils.hasOwnProp(config2, 'transitional') ? config2.transitional : undefined; + + if (!utils.isUndefined(transitional2)) { + if (utils.isPlainObject(transitional2)) { + if (utils.hasOwnProp(transitional2, prop)) { + return transitional2[prop]; + } + } else { + return undefined; + } + } + + const transitional1 = utils.hasOwnProp(config1, 'transitional') ? config1.transitional : undefined; + + if (utils.isPlainObject(transitional1) && utils.hasOwnProp(transitional1, prop)) { + return transitional1[prop]; + } + + return undefined; + } + // eslint-disable-next-line consistent-return function mergeDirectKeys(a, b, prop) { if (utils.hasOwnProp(config2, prop)) { @@ -120,5 +142,17 @@ export default function mergeConfig(config1, config2) { (utils.isUndefined(configValue) && merge !== mergeDirectKeys) || (config[prop] = configValue); }); + if ( + utils.hasOwnProp(config2, 'validateStatus') && + utils.isUndefined(config2.validateStatus) && + getMergedTransitionalOption('validateStatusUndefinedResolves') === false + ) { + if (utils.hasOwnProp(config1, 'validateStatus')) { + config.validateStatus = getMergedValue(undefined, config1.validateStatus); + } else { + delete config.validateStatus; + } + } + return config; } diff --git a/lib/defaults/transitional.js b/lib/defaults/transitional.js index de2c0d92..e64a6a99 100644 --- a/lib/defaults/transitional.js +++ b/lib/defaults/transitional.js @@ -6,4 +6,5 @@ export default { clarifyTimeoutError: false, legacyInterceptorReqResOrdering: true, advertiseZstdAcceptEncoding: false, + validateStatusUndefinedResolves: true, }; diff --git a/tests/browser/requests.browser.test.js b/tests/browser/requests.browser.test.js index ab76dd38..3bb07b52 100644 --- a/tests/browser/requests.browser.test.js +++ b/tests/browser/requests.browser.test.js @@ -283,7 +283,7 @@ describe('requests (vitest browser)', () => { await expect(promise).resolves.toBeDefined(); }); - it('should resolve when validateStatus is undefined', async () => { + it('should resolve when validateStatus is undefined by default', async () => { const { request, promise } = startRequest('/foo', { validateStatus: undefined, }); @@ -292,6 +292,23 @@ describe('requests (vitest browser)', () => { await expect(promise).resolves.toBeDefined(); }); + // https://github.com/axios/axios/issues/6688 + it('should reject when validateStatus is undefined and the transitional option is disabled', async () => { + const { request, promise } = startRequest('/foo', { + validateStatus: undefined, + transitional: { validateStatusUndefinedResolves: false }, + }); + + request.respondWith({ status: 500 }); + const reason = await promise.catch((error) => error); + + expect(reason).toBeInstanceOf(Error); + expect(reason.message).toBe('Request failed with status code 500'); + expect(reason.config.method).toBe('get'); + expect(reason.config.url).toBe('/foo'); + expect(reason.response.status).toBe(500); + }); + // https://github.com/axios/axios/issues/378 it('should return JSON when rejecting', async () => { const { request, promise } = startRequest( diff --git a/tests/unit/core/mergeConfig.test.js b/tests/unit/core/mergeConfig.test.js index 5150a8e4..8b377929 100644 --- a/tests/unit/core/mergeConfig.test.js +++ b/tests/unit/core/mergeConfig.test.js @@ -353,5 +353,25 @@ describe('core::mergeConfig', () => { expect(mergeConfig({ validateStatus: obj }, {}).validateStatus).toBe(obj); expect(mergeConfig({ validateStatus: null }, {}).validateStatus).toBe(null); }); + + it('keeps legacy undefined behavior by default', () => { + expect(mergeConfig(defaults, { validateStatus: undefined }).validateStatus).toBeUndefined(); + }); + + it('keeps config1 value when the transitional option is disabled (issue #6688)', () => { + const validateStatus = () => false; + + expect( + mergeConfig( + { validateStatus }, + { + validateStatus: undefined, + transitional: { validateStatusUndefinedResolves: false }, + } + ).validateStatus + ).toBe(validateStatus); + + expect(mergeConfig(defaults, { validateStatus: null }).validateStatus).toBe(null); + }); }); });