Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 4 additions & 0 deletions packages/api-fetch/CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,10 @@

- Raise the minimum required Node.js version to `>=22.13.0`, for stable `require(esm)` support ([#84313](https://github.com/WordPress/gutenberg/pull/84313)).

### Enhancements

- Include the response's status as `data.status` in `invalid_json` errors, and in error responses that have no `data` ([#85157](https://github.com/WordPress/gutenberg/pull/85157)).

## 7.57.0 (2026-10-07)

## 7.56.0 (2026-09-23)
Expand Down
26 changes: 26 additions & 0 deletions packages/api-fetch/src/test/index.jsdom.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -131,6 +131,27 @@ describe( 'apiFetch', () => {
await expect( apiFetch( { path: '/random' } ) ).rejects.toEqual( {
code: 'bad_request',
message: 'Bad Request',
data: { status: 400 },
} );
} );

it( 'should keep the data of an error response', async () => {
globalThis.fetch.mockResolvedValue( {
ok: false,
status: 500,
async json() {
return {
code: 'db_update_error',
message: 'Could not update post in the database.',
data: 'Table does not exist',
};
},
} );

await expect( apiFetch( { path: '/random' } ) ).rejects.toEqual( {
code: 'db_update_error',
message: 'Could not update post in the database.',
data: 'Table does not exist',
} );
} );

Expand All @@ -146,6 +167,7 @@ describe( 'apiFetch', () => {
await expect( apiFetch( { path: '/random' } ) ).rejects.toEqual( {
code: 'invalid_json',
message: 'The response is not a valid JSON response.',
data: { status: 200 },
} );
} );

Expand All @@ -161,6 +183,7 @@ describe( 'apiFetch', () => {
await expect( apiFetch( { path: '/random' } ) ).rejects.toEqual( {
code: 'invalid_json',
message: 'The response is not a valid JSON response.',
data: { status: 200 },
} );
} );

Expand Down Expand Up @@ -234,6 +257,7 @@ describe( 'apiFetch', () => {
await expect( apiFetch( { path: '/random' } ) ).rejects.toEqual( {
code: 'invalid_json',
message: 'The response is not a valid JSON response.',
data: { status: 200 },
} );
} );

Expand All @@ -245,6 +269,7 @@ describe( 'apiFetch', () => {
await expect( apiFetch( { path: '/random' } ) ).rejects.toEqual( {
code: 'invalid_json',
message: 'The response is not a valid JSON response.',
data: { status: 500 },
} );
} );

Expand Down Expand Up @@ -357,6 +382,7 @@ describe( 'apiFetch', () => {

await expect( apiFetch( { path: '/random' } ) ).rejects.toEqual( {
code: 'rest_cookie_invalid_nonce',
data: { status: 403 },
} );
} );

Expand Down
14 changes: 13 additions & 1 deletion packages/api-fetch/src/utils/response.ts
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,7 @@ async function parseJsonAndNormalizeError(
throw {
code: 'invalid_json',
message: __( 'The response is not a valid JSON response.' ),
data: { status: response.status },
};
}
}
Expand Down Expand Up @@ -75,5 +76,16 @@ export async function parseAndThrowError(
}

// Parse the response JSON and throw it as an error.
throw await parseJsonAndNormalizeError( response );
const error = await parseJsonAndNormalizeError( response );

// Like WordPress's own REST errors, an error carries its status in `data`.
if (
error &&
typeof error === 'object' &&
! Array.isArray( error ) &&
error.data === undefined
) {
throw { ...error, data: { status: response.status } };
}
throw error;
}
29 changes: 24 additions & 5 deletions packages/editor/src/store/actions.js
Original file line number Diff line number Diff line change
Expand Up @@ -23,7 +23,7 @@ import {
doActionAsync,
} from '@wordpress/hooks';
import { store as preferencesStore } from '@wordpress/preferences';
import { __ } from '@wordpress/i18n';
import { __, sprintf } from '@wordpress/i18n';
import { localAutosaveSet } from './local-autosave';
import {
getNotificationArgumentsForSaveSuccess,
Expand All @@ -39,9 +39,13 @@ import { AUTOSAVE_RETRY_SLOT } from './constants';
const { getEntitySnapshot } = unlock( coreDataPrivateApis );

// Error codes whose message is generated by the client rather than returned by
// the server. Their messages restate what the notice already says, so there are
// no details worth disclosing.
const CLIENT_GENERATED_ERROR_CODES = [ 'offline_error', 'fetch_error' ];
// the server. Their messages add nothing to the notice, so they are not shown as
// details.
const CLIENT_GENERATED_ERROR_CODES = [
'offline_error',
'fetch_error',
'invalid_json',
];

// Block-level elements whose end is a line break once markup is removed, so two
// sentences in separate paragraphs don't run together.
Expand Down Expand Up @@ -408,11 +412,26 @@ export const savePost =
// A PHP fatal error's message, with its stack trace, comes in
// `data.error`, apart from the generic message WordPress shows.
const phpError = error.data?.error?.message;
const details = [
const status = error.data?.status;
const message =
error.message &&
! CLIENT_GENERATED_ERROR_CODES.includes( error.code )
? getFailureDetails( error.message )
: '';
const summary = [
Number.isInteger( status ) && status >= 400
? sprintf(
/* translators: %d: HTTP status code, such as 500. */
__( 'Error code %d.' ),
status
)
: '',
message,
]
.filter( Boolean )
.join( ' ' );
const details = [
summary,
typeof phpError === 'string' ? phpError.trim() : '',
]
.filter( Boolean )
Expand Down
22 changes: 20 additions & 2 deletions packages/editor/src/store/test/actions.jsdom.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -427,7 +427,7 @@ describe( 'Post actions', () => {
},
},
},
'There has been a critical error on this website.\n\nUncaught Error: Call to undefined function f() in /var/www/html/a.php:2\nStack trace:\n#0 {main}\n thrown',
'Error code 500. There has been a critical error on this website.\n\nUncaught Error: Call to undefined function f() in /var/www/html/a.php:2\nStack trace:\n#0 {main}\n thrown',
],
[
'ignores a PHP error message that is not a string',
Expand All @@ -436,7 +436,25 @@ describe( 'Post actions', () => {
message: 'Server error.',
data: { status: 500, error: { message: [ 'unexpected' ] } },
},
'Server error.',
'Error code 500. Server error.',
],
[
'shows the status of a response that is not JSON',
{
code: 'invalid_json',
message: 'The response is not a valid JSON response.',
data: { status: 502 },
},
'Error code 502.',
],
[
'shows no details for a successful response that is not JSON',
{
code: 'invalid_json',
message: 'The response is not a valid JSON response.',
data: { status: 200 },
},
undefined,
],
] )( '%s', async ( _description, serverError, expectedDetails ) => {
const post = {
Expand Down
9 changes: 8 additions & 1 deletion packages/editor/src/store/utils/notice-builder.js
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,10 @@ const AUTO_SAVE_FAILURE_NOTICE = __(
'Auto-save failed. You can also save manually.'
);

const AUTO_SAVE_NO_RESPONSE_FAILURE_NOTICE = __(
'Auto-save failed because there was no response from the server. You can also save manually.'
);

const AUTO_SAVE_OFFLINE_FAILURE_NOTICE = __(
'Auto-save failed because you were offline. You can also save manually once you’re back online.'
);
Expand Down Expand Up @@ -150,7 +154,10 @@ export function getNotificationArgumentsForSaveFail( data ) {
: messages.default;

if ( options?.isAutosave ) {
noticeMessage = AUTO_SAVE_FAILURE_NOTICE;
noticeMessage =
error.code === 'fetch_error'
? AUTO_SAVE_NO_RESPONSE_FAILURE_NOTICE
: AUTO_SAVE_FAILURE_NOTICE;
}

return [ noticeMessage, { id } ];
Expand Down
10 changes: 10 additions & 0 deletions packages/editor/src/store/utils/test/notice-builder.js
Original file line number Diff line number Diff line change
Expand Up @@ -158,6 +158,16 @@ describe( 'getNotificationArgumentsForSaveFail()', () => {
],
{ isAutosave: true },
],
[
'when the autosave got no response',
'fetch_error',
[ 'publish', 'publish' ],
[
'Auto-save failed because there was no response from the server. You can also save manually.',
{ id: 'editor-autosave' },
],
{ isAutosave: true },
],
].forEach(
( [
description,
Expand Down
Loading
Loading