mirror of
https://github.com/tenrok/axios.git
synced 2026-06-17 19:21:29 +03:00
fix(dispatchRequest): attach response to AxiosError on JSON parse failure (#10724)
* fix(dispatchRequest): attach response to AxiosError on JSON parse failure When JSON.parse fails in strict mode (responseType: 'json' or silentJSONParsing: false), the thrown AxiosError was missing both error.response and error.status because transformResponse has no direct access to the response object — only the config is available as `this` inside transformData. Root cause: transformData is called as transformData.call(config, ...) and AxiosError.from uses `this.response` to populate the error's response field, but config.response was never set. Fix: in dispatchRequest, temporarily assign the response object to config.response before invoking transformData, then clean it up in a finally block to avoid permanently polluting the config object. This ensures error.response and error.status are available when a request with responseType='json' receives a malformed JSON body, making the error consistent with all other AxiosError instances. Fixes #7224 * chore: added additional testing and removed the misleading test --------- Co-authored-by: AKIBUZZAMAN AKIB <akibuzzamanakib473@gmail.com> Co-authored-by: Jason Saayman <jasonsaayman@gmail.com>
This commit is contained in:
@@ -49,8 +49,15 @@ export default function dispatchRequest(config) {
|
||||
function onAdapterResolution(response) {
|
||||
throwIfCancellationRequested(config);
|
||||
|
||||
// Transform response data
|
||||
response.data = transformData.call(config, config.transformResponse, response);
|
||||
// Expose the current response on config so that transformResponse can
|
||||
// attach it to any AxiosError it throws (e.g. on JSON parse failure).
|
||||
// We clean it up afterwards to avoid polluting the config object.
|
||||
config.response = response;
|
||||
try {
|
||||
response.data = transformData.call(config, config.transformResponse, response);
|
||||
} finally {
|
||||
delete config.response;
|
||||
}
|
||||
|
||||
response.headers = AxiosHeaders.from(response.headers);
|
||||
|
||||
@@ -62,11 +69,16 @@ export default function dispatchRequest(config) {
|
||||
|
||||
// Transform response data
|
||||
if (reason && reason.response) {
|
||||
reason.response.data = transformData.call(
|
||||
config,
|
||||
config.transformResponse,
|
||||
reason.response
|
||||
);
|
||||
config.response = reason.response;
|
||||
try {
|
||||
reason.response.data = transformData.call(
|
||||
config,
|
||||
config.transformResponse,
|
||||
reason.response
|
||||
);
|
||||
} finally {
|
||||
delete config.response;
|
||||
}
|
||||
reason.response.headers = AxiosHeaders.from(reason.response.headers);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,146 @@
|
||||
import { describe, it } from 'vitest';
|
||||
import assert from 'assert';
|
||||
import dispatchRequest from '../../../lib/core/dispatchRequest.js';
|
||||
import AxiosError from '../../../lib/core/AxiosError.js';
|
||||
import defaults from '../../../lib/defaults/index.js';
|
||||
|
||||
function baseConfig(overrides = {}) {
|
||||
return {
|
||||
method: 'get',
|
||||
url: '/test',
|
||||
headers: {},
|
||||
transformRequest: defaults.transformRequest,
|
||||
transformResponse: defaults.transformResponse,
|
||||
transitional: { silentJSONParsing: false, forcedJSONParsing: true },
|
||||
responseType: 'json',
|
||||
...overrides,
|
||||
};
|
||||
}
|
||||
|
||||
describe('core::dispatchRequest', () => {
|
||||
describe('JSON parse failure on adapter resolution', () => {
|
||||
it('rejects with AxiosError carrying response and status', async () => {
|
||||
const response = {
|
||||
data: '{bad json',
|
||||
status: 418,
|
||||
statusText: "I'm a teapot",
|
||||
headers: {},
|
||||
config: null,
|
||||
request: {},
|
||||
};
|
||||
const config = baseConfig({ adapter: () => Promise.resolve(response) });
|
||||
|
||||
let thrown;
|
||||
try {
|
||||
await dispatchRequest(config);
|
||||
} catch (e) {
|
||||
thrown = e;
|
||||
}
|
||||
|
||||
assert.ok(thrown instanceof AxiosError, 'must be AxiosError');
|
||||
assert.strictEqual(thrown.code, AxiosError.ERR_BAD_RESPONSE);
|
||||
assert.strictEqual(thrown.response, response, 'error.response must be the original response');
|
||||
assert.strictEqual(thrown.status, 418, 'error.status must equal response status');
|
||||
});
|
||||
|
||||
it('cleans up config.response after the transform throws', async () => {
|
||||
const response = {
|
||||
data: '{bad json',
|
||||
status: 200,
|
||||
statusText: 'OK',
|
||||
headers: {},
|
||||
config: null,
|
||||
request: {},
|
||||
};
|
||||
const config = baseConfig({ adapter: () => Promise.resolve(response) });
|
||||
|
||||
try {
|
||||
await dispatchRequest(config);
|
||||
} catch (_) {
|
||||
// expected
|
||||
}
|
||||
|
||||
assert.strictEqual(
|
||||
Object.prototype.hasOwnProperty.call(config, 'response'),
|
||||
false,
|
||||
'config.response must be deleted in finally'
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
describe('JSON parse failure on adapter rejection', () => {
|
||||
it('rejects with AxiosError carrying response and status (rejection path)', async () => {
|
||||
const response = {
|
||||
data: '{bad json',
|
||||
status: 500,
|
||||
statusText: 'Internal Server Error',
|
||||
headers: {},
|
||||
config: null,
|
||||
request: {},
|
||||
};
|
||||
const reason = new AxiosError('Request failed', AxiosError.ERR_BAD_RESPONSE);
|
||||
reason.response = response;
|
||||
const config = baseConfig({ adapter: () => Promise.reject(reason) });
|
||||
|
||||
let thrown;
|
||||
try {
|
||||
await dispatchRequest(config);
|
||||
} catch (e) {
|
||||
thrown = e;
|
||||
}
|
||||
|
||||
assert.ok(thrown instanceof AxiosError, 'must be AxiosError');
|
||||
assert.strictEqual(thrown.response, response, 'error.response must be the original response');
|
||||
assert.strictEqual(thrown.status, 500, 'error.status must equal response status');
|
||||
});
|
||||
|
||||
it('cleans up config.response after the rejection-path transform', async () => {
|
||||
const response = {
|
||||
data: '{bad json',
|
||||
status: 500,
|
||||
statusText: 'Internal Server Error',
|
||||
headers: {},
|
||||
config: null,
|
||||
request: {},
|
||||
};
|
||||
const reason = new AxiosError('Request failed', AxiosError.ERR_BAD_RESPONSE);
|
||||
reason.response = response;
|
||||
const config = baseConfig({ adapter: () => Promise.reject(reason) });
|
||||
|
||||
try {
|
||||
await dispatchRequest(config);
|
||||
} catch (_) {
|
||||
// expected
|
||||
}
|
||||
|
||||
assert.strictEqual(
|
||||
Object.prototype.hasOwnProperty.call(config, 'response'),
|
||||
false,
|
||||
'config.response must be deleted in finally on the rejection path'
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
describe('happy path', () => {
|
||||
it('cleans up config.response after a successful resolution', async () => {
|
||||
const response = {
|
||||
data: '{"ok":true}',
|
||||
status: 200,
|
||||
statusText: 'OK',
|
||||
headers: {},
|
||||
config: null,
|
||||
request: {},
|
||||
};
|
||||
const config = baseConfig({ adapter: () => Promise.resolve(response) });
|
||||
|
||||
const result = await dispatchRequest(config);
|
||||
|
||||
assert.deepStrictEqual(result.data, { ok: true });
|
||||
assert.strictEqual(
|
||||
Object.prototype.hasOwnProperty.call(config, 'response'),
|
||||
false,
|
||||
'config.response must not be left set after a successful request'
|
||||
);
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -1,6 +1,7 @@
|
||||
import { describe, it } from 'vitest';
|
||||
import defaults from '../../lib/defaults/index.js';
|
||||
import transformData from '../../lib/core/transformData.js';
|
||||
import AxiosError from '../../lib/core/AxiosError.js';
|
||||
import assert from 'assert';
|
||||
|
||||
describe('transformResponse', () => {
|
||||
@@ -36,6 +37,46 @@ describe('transformResponse', () => {
|
||||
});
|
||||
});
|
||||
|
||||
describe('malformed JSON with responseType: json', () => {
|
||||
it('throws AxiosError with ERR_BAD_RESPONSE code', () => {
|
||||
const response = { status: 200, headers: {}, data: '{bad json' };
|
||||
const config = {
|
||||
responseType: 'json',
|
||||
transitional: { silentJSONParsing: false, forcedJSONParsing: true },
|
||||
response,
|
||||
};
|
||||
|
||||
assert.throws(
|
||||
() => transformData.call(config, defaults.transformResponse, response),
|
||||
(e) => e instanceof AxiosError && e.code === AxiosError.ERR_BAD_RESPONSE
|
||||
);
|
||||
});
|
||||
|
||||
it('attaches response to AxiosError so error.status and error.response are available', () => {
|
||||
// Regression test for https://github.com/axios/axios/issues/7224
|
||||
// When JSON.parse fails in strict mode, the thrown AxiosError must carry
|
||||
// the original response so callers can inspect error.status and
|
||||
// error.response without having to re-examine the raw response.
|
||||
const response = { status: 200, headers: {}, data: '{bad json' };
|
||||
const config = {
|
||||
responseType: 'json',
|
||||
transitional: { silentJSONParsing: false, forcedJSONParsing: true },
|
||||
response,
|
||||
};
|
||||
|
||||
let thrown;
|
||||
try {
|
||||
transformData.call(config, defaults.transformResponse, response);
|
||||
} catch (e) {
|
||||
thrown = e;
|
||||
}
|
||||
|
||||
assert.ok(thrown instanceof AxiosError, 'must be AxiosError');
|
||||
assert.strictEqual(thrown.status, 200, 'error.status must equal response status');
|
||||
assert.strictEqual(thrown.response, response, 'error.response must be the original response');
|
||||
});
|
||||
});
|
||||
|
||||
describe('204 request', () => {
|
||||
it('does not parse the empty string', () => {
|
||||
const data = '';
|
||||
|
||||
Reference in New Issue
Block a user