From 72fe8b927bfdab9bd90c4ccb05a6c739e6c40d5f Mon Sep 17 00:00:00 2001 From: ella Date: Mon, 19 Oct 2026 15:17:26 -0700 Subject: [PATCH 1/6] Say when an autosave got no response from the server When the request fails before any response arrives, the notice now says "Auto-save failed because there was no response from the server." instead of only "Auto-save failed." Co-Authored-By: Claude Opus 5.5 --- .../editor/src/store/utils/notice-builder.js | 9 ++++++- .../src/store/utils/test/notice-builder.js | 10 ++++++++ .../various/autosave-failure-notice.spec.js | 24 ++++++++++--------- 3 files changed, 31 insertions(+), 12 deletions(-) diff --git a/packages/editor/src/store/utils/notice-builder.js b/packages/editor/src/store/utils/notice-builder.js index 0713cdee670900..8c15714a22bf8a 100644 --- a/packages/editor/src/store/utils/notice-builder.js +++ b/packages/editor/src/store/utils/notice-builder.js @@ -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.' ); @@ -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 } ]; diff --git a/packages/editor/src/store/utils/test/notice-builder.js b/packages/editor/src/store/utils/test/notice-builder.js index 58036303e017d3..244baa47b3b94b 100644 --- a/packages/editor/src/store/utils/test/notice-builder.js +++ b/packages/editor/src/store/utils/test/notice-builder.js @@ -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, diff --git a/test/e2e/specs/editor/various/autosave-failure-notice.spec.js b/test/e2e/specs/editor/various/autosave-failure-notice.spec.js index 0a44bb78bdeecf..879da034d680e9 100644 --- a/test/e2e/specs/editor/various/autosave-failure-notice.spec.js +++ b/test/e2e/specs/editor/various/autosave-failure-notice.spec.js @@ -5,6 +5,9 @@ const { const AUTOSAVE_FAILED = 'Auto-save failed. You can also save manually.'; +const AUTOSAVE_FAILED_NO_RESPONSE = + 'Auto-save failed because there was no response from the server. You can also save manually.'; + const UPDATING_FAILED_OFFLINE = 'Updating failed because you were offline. Please verify your connection and try again.'; @@ -96,11 +99,11 @@ test.describe( 'Autosave failure notice', () => { 'link-in-message', withDetails( 'Blocked. Learn more (https://example.com).' ), ], - // TODO: Say the server could not be reached, and retry right away. + // TODO: Retry right away. [ 'connection dropped before a response', 'dropped-connection', - AUTOSAVE_FAILED, + AUTOSAVE_FAILED_NO_RESPONSE, ], // TODO: Show the status code. [ 'error without a message', 'no-message', AUTOSAVE_FAILED ], @@ -143,11 +146,10 @@ test.describe( 'Autosave failure notice', () => { 'redirect-to-login', withDetails( 'The response is not a valid JSON response.' ), ], - // TODO: Say the server could not be reached. [ 'redirected to another site', 'redirect-to-other-site', - AUTOSAVE_FAILED, + AUTOSAVE_FAILED_NO_RESPONSE, ], [ 'maintenance mode', @@ -298,11 +300,11 @@ test.describe( 'Autosave failure notice', () => { .locator( '.notices-inline-notices__notice' ); await expect( notice ).toContainText( new RegExp( - `${ AUTOSAVE_FAILED.replaceAll( '.', '\\.' ) } Retrying in (10|9) seconds\\.` + `${ AUTOSAVE_FAILED_NO_RESPONSE.replaceAll( '.', '\\.' ) } Retrying in (10|9) seconds\\.` ) ); await expect( notice ).toContainText( - `${ AUTOSAVE_FAILED } Retrying in 7 seconds.` + `${ AUTOSAVE_FAILED_NO_RESPONSE } Retrying in 7 seconds.` ); } ); @@ -323,7 +325,7 @@ test.describe( 'Autosave failure notice', () => { .getByRole( 'region', { name: 'Editor content' } ) .locator( '.notices-inline-notices__notice' ); await expect( notice ).toContainText( - `${ AUTOSAVE_FAILED } Retrying…` + `${ AUTOSAVE_FAILED_NO_RESPONSE } Retrying…` ); await expect( notice ).toHaveCount( 0 ); } ); @@ -355,7 +357,7 @@ test.describe( 'Autosave failure notice', () => { } } expect( await autosaveFailure.getNotices() ).toEqual( [ - AUTOSAVE_FAILED, + AUTOSAVE_FAILED_NO_RESPONSE, ] ); } ); } @@ -366,7 +368,7 @@ test.describe( 'Autosave failure notice', () => { await autosaveFailure.fail( 'dropped-connection' ); await autosaveFailure.autosave(); expect( await autosaveFailure.getNotices() ).toEqual( [ - AUTOSAVE_FAILED, + AUTOSAVE_FAILED_NO_RESPONSE, ] ); await autosaveFailure.stopFailing(); @@ -381,7 +383,7 @@ test.describe( 'Autosave failure notice', () => { await autosaveFailure.fail( 'dropped-connection' ); await autosaveFailure.autosave(); expect( await autosaveFailure.getNotices() ).toEqual( [ - AUTOSAVE_FAILED, + AUTOSAVE_FAILED_NO_RESPONSE, ] ); await autosaveFailure.stopFailing(); @@ -400,7 +402,7 @@ test.describe( 'Autosave failure notice', () => { await autosaveFailure.fail( 'dropped-connection' ); await autosaveFailure.autosave(); expect( await autosaveFailure.getNotices() ).toEqual( [ - AUTOSAVE_FAILED, + AUTOSAVE_FAILED_NO_RESPONSE, ] ); await context.setOffline( true ); From 583a8a4a714e6208d56e91d6f48fd668fce3007e Mon Sep 17 00:00:00 2001 From: ella Date: Mon, 19 Oct 2026 17:45:04 -0700 Subject: [PATCH 2/6] Show the HTTP status in save failure details apiFetch now keeps the response's status in the error's `data` when the body doesn't include one, and the address a redirected request ended at for `invalid_json` errors. The save failure details show "Error 502" and so on instead of "The response is not a valid JSON response.", and an autosave redirected to the login page says you may have been logged out. Co-Authored-By: Claude Opus 5.5 --- packages/api-fetch/CHANGELOG.md | 4 ++ .../api-fetch/src/test/index.jsdom.test.js | 61 +++++++++++++++++++ packages/api-fetch/src/utils/response.ts | 43 ++++++++++++- packages/editor/src/store/actions.js | 20 ++++-- .../src/store/test/actions.jsdom.test.js | 22 ++++++- .../editor/src/store/utils/notice-builder.js | 31 ++++++++-- .../src/store/utils/test/notice-builder.js | 16 +++++ .../various/autosave-failure-notice.spec.js | 43 +++++++------ 8 files changed, 209 insertions(+), 31 deletions(-) diff --git a/packages/api-fetch/CHANGELOG.md b/packages/api-fetch/CHANGELOG.md index 5fb351605b48ac..2e60545b9d5df3 100644 --- a/packages/api-fetch/CHANGELOG.md +++ b/packages/api-fetch/CHANGELOG.md @@ -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 + +- Add the response's status to the `data` of a thrown error when the error body doesn't include one, including `invalid_json` errors, which also include the address a redirected request ended at as `data.url` ([#85157](https://github.com/WordPress/gutenberg/pull/85157)). + ## 7.57.0 (2026-10-07) ## 7.56.0 (2026-09-23) diff --git a/packages/api-fetch/src/test/index.jsdom.test.js b/packages/api-fetch/src/test/index.jsdom.test.js index f848e55c43c88e..fa48ac9b2f12d6 100644 --- a/packages/api-fetch/src/test/index.jsdom.test.js +++ b/packages/api-fetch/src/test/index.jsdom.test.js @@ -131,6 +131,47 @@ describe( 'apiFetch', () => { await expect( apiFetch( { path: '/random' } ) ).rejects.toEqual( { code: 'bad_request', message: 'Bad Request', + data: { status: 400 }, + } ); + } ); + + it( 'should add the status to 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: { reason: 'test' }, + }; + }, + } ); + + await expect( apiFetch( { path: '/random' } ) ).rejects.toEqual( { + code: 'db_update_error', + message: 'Could not update post in the database.', + data: { reason: 'test', status: 500 }, + } ); + } ); + + it( 'should keep the data of an error response', async () => { + globalThis.fetch.mockResolvedValue( { + ok: false, + status: 403, + async json() { + return { + code: 'rest_forbidden', + message: 'Forbidden', + data: { status: 401 }, + }; + }, + } ); + + await expect( apiFetch( { path: '/random' } ) ).rejects.toEqual( { + code: 'rest_forbidden', + message: 'Forbidden', + data: { status: 401 }, } ); } ); @@ -146,6 +187,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 }, } ); } ); @@ -161,6 +203,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 }, } ); } ); @@ -234,6 +277,22 @@ describe( 'apiFetch', () => { await expect( apiFetch( { path: '/random' } ) ).rejects.toEqual( { code: 'invalid_json', message: 'The response is not a valid JSON response.', + data: { status: 200 }, + } ); + } ); + + it( 'should return the redirect address with an invalid JSON error', async () => { + const response = new Response( '', { status: 200 } ); + Object.defineProperties( response, { + redirected: { value: true }, + url: { value: 'http://example.com/wp-login.php' }, + } ); + globalThis.fetch.mockResolvedValue( response ); + + await expect( apiFetch( { path: '/random' } ) ).rejects.toEqual( { + code: 'invalid_json', + message: 'The response is not a valid JSON response.', + data: { status: 200, url: 'http://example.com/wp-login.php' }, } ); } ); @@ -245,6 +304,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 }, } ); } ); @@ -357,6 +417,7 @@ describe( 'apiFetch', () => { await expect( apiFetch( { path: '/random' } ) ).rejects.toEqual( { code: 'rest_cookie_invalid_nonce', + data: { status: 403 }, } ); } ); diff --git a/packages/api-fetch/src/utils/response.ts b/packages/api-fetch/src/utils/response.ts index 358856718470a1..eb0b607a6fef85 100644 --- a/packages/api-fetch/src/utils/response.ts +++ b/packages/api-fetch/src/utils/response.ts @@ -32,10 +32,40 @@ async function parseJsonAndNormalizeError( throw { code: 'invalid_json', message: __( 'The response is not a valid JSON response.' ), + ...getResponseData( response ), }; } } +/** + * Returns the response's status, and the address it was redirected to, in the + * shape of a REST error's `data`. + * + * @param response + * @return Object with a `data` property, or an empty object. + */ +function getResponseData( response: Response ) { + if ( typeof response.status !== 'number' ) { + return {}; + } + return { + data: { + status: response.status, + ...( response.redirected && { url: response.url } ), + }, + }; +} + +/** + * Returns whether a value is an object other than an array. + * + * @param value + * @return Whether the value is a plain object. + */ +function isPlainObject( value: unknown ): value is Record< string, any > { + return !! value && typeof value === 'object' && ! Array.isArray( value ); +} + /** * Parses the apiFetch response properly and normalize response errors. * @@ -75,5 +105,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 the status in `data`. + if ( + isPlainObject( error ) && + ( error.data === undefined || isPlainObject( error.data ) ) && + error.data?.status === undefined && + typeof response.status === 'number' + ) { + throw { ...error, data: { ...error.data, status: response.status } }; + } + throw error; } diff --git a/packages/editor/src/store/actions.js b/packages/editor/src/store/actions.js index d72fce8de7db81..5f94c7d6e4a4d2 100644 --- a/packages/editor/src/store/actions.js +++ b/packages/editor/src/store/actions.js @@ -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, @@ -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. @@ -408,12 +412,20 @@ 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 status = error.data?.status; const details = [ error.message && ! CLIENT_GENERATED_ERROR_CODES.includes( error.code ) ? getFailureDetails( error.message ) : '', typeof phpError === 'string' ? phpError.trim() : '', + Number.isInteger( status ) && status >= 400 + ? sprintf( + /* translators: %d: HTTP status code, such as 500. */ + __( 'Error %d' ), + status + ) + : '', ] .filter( Boolean ) .join( '\n\n' ); diff --git a/packages/editor/src/store/test/actions.jsdom.test.js b/packages/editor/src/store/test/actions.jsdom.test.js index 553f314953d7fa..61b924f0fc5c0d 100644 --- a/packages/editor/src/store/test/actions.jsdom.test.js +++ b/packages/editor/src/store/test/actions.jsdom.test.js @@ -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', + '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\n\nError 500', ], [ 'ignores a PHP error message that is not a string', @@ -436,7 +436,25 @@ describe( 'Post actions', () => { message: 'Server error.', data: { status: 500, error: { message: [ 'unexpected' ] } }, }, - 'Server error.', + 'Server error.\n\nError 500', + ], + [ + '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 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 = { diff --git a/packages/editor/src/store/utils/notice-builder.js b/packages/editor/src/store/utils/notice-builder.js index 8c15714a22bf8a..83c230c5464396 100644 --- a/packages/editor/src/store/utils/notice-builder.js +++ b/packages/editor/src/store/utils/notice-builder.js @@ -9,10 +9,30 @@ 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_LOGGED_OUT_FAILURE_NOTICE = __( + 'Auto-save failed because you may have been logged out.' +); + const AUTO_SAVE_OFFLINE_FAILURE_NOTICE = __( 'Auto-save failed because you were offline. You can also save manually once you’re back online.' ); +/** + * Returns whether an address is the login page, where a request lands once the + * session has ended. + * + * @param {string} [url] Address a request was redirected to. + * + * @return {boolean} Whether the address is the login page. + */ +function isLoginPage( url ) { + try { + return new URL( url ).pathname.endsWith( '/wp-login.php' ); + } catch { + return false; + } +} + /** * Builds the arguments for a success notification dispatch. * @@ -154,10 +174,13 @@ export function getNotificationArgumentsForSaveFail( data ) { : messages.default; if ( options?.isAutosave ) { - noticeMessage = - error.code === 'fetch_error' - ? AUTO_SAVE_NO_RESPONSE_FAILURE_NOTICE - : AUTO_SAVE_FAILURE_NOTICE; + if ( error.code === 'fetch_error' ) { + noticeMessage = AUTO_SAVE_NO_RESPONSE_FAILURE_NOTICE; + } else if ( isLoginPage( error.data?.url ) ) { + noticeMessage = AUTO_SAVE_LOGGED_OUT_FAILURE_NOTICE; + } else { + noticeMessage = AUTO_SAVE_FAILURE_NOTICE; + } } return [ noticeMessage, { id } ]; diff --git a/packages/editor/src/store/utils/test/notice-builder.js b/packages/editor/src/store/utils/test/notice-builder.js index 244baa47b3b94b..72a2d46ddc5486 100644 --- a/packages/editor/src/store/utils/test/notice-builder.js +++ b/packages/editor/src/store/utils/test/notice-builder.js @@ -168,6 +168,20 @@ describe( 'getNotificationArgumentsForSaveFail()', () => { ], { isAutosave: true }, ], + [ + 'when the autosave was redirected to the login page', + 'invalid_json', + [ 'publish', 'publish' ], + [ + 'Auto-save failed because you may have been logged out.', + { id: 'editor-autosave' }, + ], + { isAutosave: true }, + { + status: 200, + url: 'http://example.com/wp-login.php?redirect_to=x', + }, + ], ].forEach( ( [ description, @@ -175,10 +189,12 @@ describe( 'getNotificationArgumentsForSaveFail()', () => { [ postStatus, editsStatus ], expectedValue, options = {}, + data, ] ) => { it( description, () => { post.status = postStatus; error.code = errorCode; + error.data = data; edits.status = editsStatus; expect( getNotificationArgumentsForSaveFail( { diff --git a/test/e2e/specs/editor/various/autosave-failure-notice.spec.js b/test/e2e/specs/editor/various/autosave-failure-notice.spec.js index 879da034d680e9..a9b91b0636eeb5 100644 --- a/test/e2e/specs/editor/various/autosave-failure-notice.spec.js +++ b/test/e2e/specs/editor/various/autosave-failure-notice.spec.js @@ -5,6 +5,9 @@ const { const AUTOSAVE_FAILED = 'Auto-save failed. You can also save manually.'; +const AUTOSAVE_FAILED_LOGGED_OUT = + 'Auto-save failed because you may have been logged out.'; + const AUTOSAVE_FAILED_NO_RESPONSE = 'Auto-save failed because there was no response from the server. You can also save manually.'; @@ -92,12 +95,16 @@ test.describe( 'Autosave failure notice', () => { [ 'database connection lost', 'database-connection-lost', - withDetails( 'Error establishing a database connection' ), + withDetails( + 'Error establishing a database connection\n\nError 500' + ), ], [ 'error message containing a link', 'link-in-message', - withDetails( 'Blocked. Learn more (https://example.com).' ), + withDetails( + 'Blocked. Learn more (https://example.com).\n\nError 403' + ), ], // TODO: Retry right away. [ @@ -105,46 +112,42 @@ test.describe( 'Autosave failure notice', () => { 'dropped-connection', AUTOSAVE_FAILED_NO_RESPONSE, ], - // TODO: Show the status code. - [ 'error without a message', 'no-message', AUTOSAVE_FAILED ], + [ 'error without a message', 'no-message', withDetails( 'Error 500' ) ], [ 'error with a plain text message', 'plain-message', - withDetails( 'Something went wrong on the server.' ), + withDetails( 'Something went wrong on the server.\n\nError 500' ), ], [ 'permission denied', 'permission', - withDetails( 'Sorry, you are not allowed to edit this post.' ), + withDetails( + 'Sorry, you are not allowed to edit this post.\n\nError 403' + ), ], + // The error's `data` is the database error, so there is no status in it. [ 'database write failed', 'database-write', withDetails( 'Could not update post in the database.' ), ], - // TODO: Show the status code (502) instead. - [ - 'HTML error page', - 'bad-gateway', - withDetails( 'The response is not a valid JSON response.' ), - ], - // TODO: Show the status code (429) instead. + [ 'HTML error page', 'bad-gateway', withDetails( 'Error 502' ) ], [ 'too many requests', 'too-many-requests', - withDetails( 'The response is not a valid JSON response.' ), + withDetails( 'Error 429' ), ], + // The error is printed before the headers, so the status stays 200. // TODO: Show the PHP error the response includes in `data.error`. [ 'PHP fatal error printed before the JSON', 'fatal-error-displayed', - withDetails( 'The response is not a valid JSON response.' ), + AUTOSAVE_FAILED, ], - // TODO: Say the session may have expired. [ 'redirected to the login page', 'redirect-to-login', - withDetails( 'The response is not a valid JSON response.' ), + AUTOSAVE_FAILED_LOGGED_OUT, ], [ 'redirected to another site', @@ -155,7 +158,7 @@ test.describe( 'Autosave failure notice', () => { 'maintenance mode', 'maintenance', withDetails( - 'Briefly unavailable for scheduled maintenance. Check back in a minute.' + 'Briefly unavailable for scheduled maintenance. Check back in a minute.\n\nError 503' ), ], // TODO: Report the failure; the autosave fails without a notice. @@ -210,7 +213,7 @@ test.describe( 'Autosave failure notice', () => { await autosaveFailure.autosave(); // TODO: Say the session expired and offer to log in again. expect( await autosaveFailure.getNotices() ).toEqual( [ - withDetails( 'Cookie check failed' ), + withDetails( 'Cookie check failed\n\nError 403' ), ] ); } ); @@ -223,7 +226,7 @@ test.describe( 'Autosave failure notice', () => { const title = await autosaveFailure.autosave(); // TODO: Don't report a failure; the autosave was stored. expect( await autosaveFailure.getNotices() ).toEqual( [ - withDetails( 'The response is not a valid JSON response.' ), + AUTOSAVE_FAILED, ] ); const postId = await page.evaluate( () => From a2a159e3abe4aea42561312945491913b2613d27 Mon Sep 17 00:00:00 2001 From: ella Date: Mon, 19 Oct 2026 17:53:01 -0700 Subject: [PATCH 3/6] Keep only the status on apiFetch's invalid JSON errors Drop the redirect address, the status added to other errors, and the logged-out wording. Co-Authored-By: Claude Opus 5.5 --- packages/api-fetch/CHANGELOG.md | 2 +- .../api-fetch/src/test/index.jsdom.test.js | 57 ------------------- packages/api-fetch/src/utils/response.ts | 44 +------------- .../editor/src/store/utils/notice-builder.js | 31 ++-------- .../src/store/utils/test/notice-builder.js | 16 ------ .../various/autosave-failure-notice.spec.js | 9 ++- 6 files changed, 11 insertions(+), 148 deletions(-) diff --git a/packages/api-fetch/CHANGELOG.md b/packages/api-fetch/CHANGELOG.md index 2e60545b9d5df3..1e26320480faf4 100644 --- a/packages/api-fetch/CHANGELOG.md +++ b/packages/api-fetch/CHANGELOG.md @@ -8,7 +8,7 @@ ### Enhancements -- Add the response's status to the `data` of a thrown error when the error body doesn't include one, including `invalid_json` errors, which also include the address a redirected request ended at as `data.url` ([#85157](https://github.com/WordPress/gutenberg/pull/85157)). +- Include the response's status as `data.status` in `invalid_json` errors ([#85157](https://github.com/WordPress/gutenberg/pull/85157)). ## 7.57.0 (2026-10-07) diff --git a/packages/api-fetch/src/test/index.jsdom.test.js b/packages/api-fetch/src/test/index.jsdom.test.js index fa48ac9b2f12d6..cbbf614266a45c 100644 --- a/packages/api-fetch/src/test/index.jsdom.test.js +++ b/packages/api-fetch/src/test/index.jsdom.test.js @@ -131,47 +131,6 @@ describe( 'apiFetch', () => { await expect( apiFetch( { path: '/random' } ) ).rejects.toEqual( { code: 'bad_request', message: 'Bad Request', - data: { status: 400 }, - } ); - } ); - - it( 'should add the status to 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: { reason: 'test' }, - }; - }, - } ); - - await expect( apiFetch( { path: '/random' } ) ).rejects.toEqual( { - code: 'db_update_error', - message: 'Could not update post in the database.', - data: { reason: 'test', status: 500 }, - } ); - } ); - - it( 'should keep the data of an error response', async () => { - globalThis.fetch.mockResolvedValue( { - ok: false, - status: 403, - async json() { - return { - code: 'rest_forbidden', - message: 'Forbidden', - data: { status: 401 }, - }; - }, - } ); - - await expect( apiFetch( { path: '/random' } ) ).rejects.toEqual( { - code: 'rest_forbidden', - message: 'Forbidden', - data: { status: 401 }, } ); } ); @@ -281,21 +240,6 @@ describe( 'apiFetch', () => { } ); } ); - it( 'should return the redirect address with an invalid JSON error', async () => { - const response = new Response( '', { status: 200 } ); - Object.defineProperties( response, { - redirected: { value: true }, - url: { value: 'http://example.com/wp-login.php' }, - } ); - globalThis.fetch.mockResolvedValue( response ); - - await expect( apiFetch( { path: '/random' } ) ).rejects.toEqual( { - code: 'invalid_json', - message: 'The response is not a valid JSON response.', - data: { status: 200, url: 'http://example.com/wp-login.php' }, - } ); - } ); - it( 'should return invalid JSON error for an error response with an empty body', async () => { globalThis.fetch.mockResolvedValue( new Response( '', { status: 500 } ) @@ -417,7 +361,6 @@ describe( 'apiFetch', () => { await expect( apiFetch( { path: '/random' } ) ).rejects.toEqual( { code: 'rest_cookie_invalid_nonce', - data: { status: 403 }, } ); } ); diff --git a/packages/api-fetch/src/utils/response.ts b/packages/api-fetch/src/utils/response.ts index eb0b607a6fef85..ff1d6641a1ff61 100644 --- a/packages/api-fetch/src/utils/response.ts +++ b/packages/api-fetch/src/utils/response.ts @@ -32,40 +32,11 @@ async function parseJsonAndNormalizeError( throw { code: 'invalid_json', message: __( 'The response is not a valid JSON response.' ), - ...getResponseData( response ), + data: { status: response.status }, }; } } -/** - * Returns the response's status, and the address it was redirected to, in the - * shape of a REST error's `data`. - * - * @param response - * @return Object with a `data` property, or an empty object. - */ -function getResponseData( response: Response ) { - if ( typeof response.status !== 'number' ) { - return {}; - } - return { - data: { - status: response.status, - ...( response.redirected && { url: response.url } ), - }, - }; -} - -/** - * Returns whether a value is an object other than an array. - * - * @param value - * @return Whether the value is a plain object. - */ -function isPlainObject( value: unknown ): value is Record< string, any > { - return !! value && typeof value === 'object' && ! Array.isArray( value ); -} - /** * Parses the apiFetch response properly and normalize response errors. * @@ -105,16 +76,5 @@ export async function parseAndThrowError( } // Parse the response JSON and throw it as an error. - const error = await parseJsonAndNormalizeError( response ); - - // Like WordPress's own REST errors, an error carries the status in `data`. - if ( - isPlainObject( error ) && - ( error.data === undefined || isPlainObject( error.data ) ) && - error.data?.status === undefined && - typeof response.status === 'number' - ) { - throw { ...error, data: { ...error.data, status: response.status } }; - } - throw error; + throw await parseJsonAndNormalizeError( response ); } diff --git a/packages/editor/src/store/utils/notice-builder.js b/packages/editor/src/store/utils/notice-builder.js index 83c230c5464396..8c15714a22bf8a 100644 --- a/packages/editor/src/store/utils/notice-builder.js +++ b/packages/editor/src/store/utils/notice-builder.js @@ -9,30 +9,10 @@ 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_LOGGED_OUT_FAILURE_NOTICE = __( - 'Auto-save failed because you may have been logged out.' -); - const AUTO_SAVE_OFFLINE_FAILURE_NOTICE = __( 'Auto-save failed because you were offline. You can also save manually once you’re back online.' ); -/** - * Returns whether an address is the login page, where a request lands once the - * session has ended. - * - * @param {string} [url] Address a request was redirected to. - * - * @return {boolean} Whether the address is the login page. - */ -function isLoginPage( url ) { - try { - return new URL( url ).pathname.endsWith( '/wp-login.php' ); - } catch { - return false; - } -} - /** * Builds the arguments for a success notification dispatch. * @@ -174,13 +154,10 @@ export function getNotificationArgumentsForSaveFail( data ) { : messages.default; if ( options?.isAutosave ) { - if ( error.code === 'fetch_error' ) { - noticeMessage = AUTO_SAVE_NO_RESPONSE_FAILURE_NOTICE; - } else if ( isLoginPage( error.data?.url ) ) { - noticeMessage = AUTO_SAVE_LOGGED_OUT_FAILURE_NOTICE; - } else { - noticeMessage = AUTO_SAVE_FAILURE_NOTICE; - } + noticeMessage = + error.code === 'fetch_error' + ? AUTO_SAVE_NO_RESPONSE_FAILURE_NOTICE + : AUTO_SAVE_FAILURE_NOTICE; } return [ noticeMessage, { id } ]; diff --git a/packages/editor/src/store/utils/test/notice-builder.js b/packages/editor/src/store/utils/test/notice-builder.js index 72a2d46ddc5486..244baa47b3b94b 100644 --- a/packages/editor/src/store/utils/test/notice-builder.js +++ b/packages/editor/src/store/utils/test/notice-builder.js @@ -168,20 +168,6 @@ describe( 'getNotificationArgumentsForSaveFail()', () => { ], { isAutosave: true }, ], - [ - 'when the autosave was redirected to the login page', - 'invalid_json', - [ 'publish', 'publish' ], - [ - 'Auto-save failed because you may have been logged out.', - { id: 'editor-autosave' }, - ], - { isAutosave: true }, - { - status: 200, - url: 'http://example.com/wp-login.php?redirect_to=x', - }, - ], ].forEach( ( [ description, @@ -189,12 +175,10 @@ describe( 'getNotificationArgumentsForSaveFail()', () => { [ postStatus, editsStatus ], expectedValue, options = {}, - data, ] ) => { it( description, () => { post.status = postStatus; error.code = errorCode; - error.data = data; edits.status = editsStatus; expect( getNotificationArgumentsForSaveFail( { diff --git a/test/e2e/specs/editor/various/autosave-failure-notice.spec.js b/test/e2e/specs/editor/various/autosave-failure-notice.spec.js index a9b91b0636eeb5..fa04e9f931751b 100644 --- a/test/e2e/specs/editor/various/autosave-failure-notice.spec.js +++ b/test/e2e/specs/editor/various/autosave-failure-notice.spec.js @@ -5,9 +5,6 @@ const { const AUTOSAVE_FAILED = 'Auto-save failed. You can also save manually.'; -const AUTOSAVE_FAILED_LOGGED_OUT = - 'Auto-save failed because you may have been logged out.'; - const AUTOSAVE_FAILED_NO_RESPONSE = 'Auto-save failed because there was no response from the server. You can also save manually.'; @@ -112,7 +109,8 @@ test.describe( 'Autosave failure notice', () => { 'dropped-connection', AUTOSAVE_FAILED_NO_RESPONSE, ], - [ 'error without a message', 'no-message', withDetails( 'Error 500' ) ], + // TODO: Show the status code. + [ 'error without a message', 'no-message', AUTOSAVE_FAILED ], [ 'error with a plain text message', 'plain-message', @@ -144,10 +142,11 @@ test.describe( 'Autosave failure notice', () => { 'fatal-error-displayed', AUTOSAVE_FAILED, ], + // TODO: Say the session may have expired. [ 'redirected to the login page', 'redirect-to-login', - AUTOSAVE_FAILED_LOGGED_OUT, + AUTOSAVE_FAILED, ], [ 'redirected to another site', From db6ec8e62959e4cb78723d2562db422736c890a9 Mon Sep 17 00:00:00 2001 From: ella Date: Mon, 19 Oct 2026 17:57:44 -0700 Subject: [PATCH 4/6] Start the details with the error code Co-Authored-By: Claude Opus 5.5 --- packages/editor/src/store/actions.js | 24 +++++++++++++------ .../src/store/test/actions.jsdom.test.js | 6 ++--- .../various/autosave-failure-notice.spec.js | 20 +++++++++------- 3 files changed, 31 insertions(+), 19 deletions(-) diff --git a/packages/editor/src/store/actions.js b/packages/editor/src/store/actions.js index 5f94c7d6e4a4d2..b058adfa885116 100644 --- a/packages/editor/src/store/actions.js +++ b/packages/editor/src/store/actions.js @@ -413,19 +413,29 @@ export const savePost = // `data.error`, apart from the generic message WordPress shows. const phpError = error.data?.error?.message; const status = error.data?.status; - const details = [ + const message = error.message && ! CLIENT_GENERATED_ERROR_CODES.includes( error.code ) ? getFailureDetails( error.message ) - : '', - typeof phpError === 'string' ? phpError.trim() : '', - Number.isInteger( status ) && status >= 400 + : ''; + let summary = message; + if ( Number.isInteger( status ) && status >= 400 ) { + summary = message ? sprintf( + /* translators: 1: HTTP status code, such as 500. 2: Error message. */ + __( 'Error code %1$d: %2$s' ), + status, + message + ) + : sprintf( /* translators: %d: HTTP status code, such as 500. */ - __( 'Error %d' ), + __( 'Error code %d' ), status - ) - : '', + ); + } + const details = [ + summary, + typeof phpError === 'string' ? phpError.trim() : '', ] .filter( Boolean ) .join( '\n\n' ); diff --git a/packages/editor/src/store/test/actions.jsdom.test.js b/packages/editor/src/store/test/actions.jsdom.test.js index 61b924f0fc5c0d..c028d7a48636f8 100644 --- a/packages/editor/src/store/test/actions.jsdom.test.js +++ b/packages/editor/src/store/test/actions.jsdom.test.js @@ -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\n\nError 500', + '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', @@ -436,7 +436,7 @@ describe( 'Post actions', () => { message: 'Server error.', data: { status: 500, error: { message: [ 'unexpected' ] } }, }, - 'Server error.\n\nError 500', + 'Error code 500: Server error.', ], [ 'shows the status of a response that is not JSON', @@ -445,7 +445,7 @@ describe( 'Post actions', () => { message: 'The response is not a valid JSON response.', data: { status: 502 }, }, - 'Error 502', + 'Error code 502', ], [ 'shows no details for a successful response that is not JSON', diff --git a/test/e2e/specs/editor/various/autosave-failure-notice.spec.js b/test/e2e/specs/editor/various/autosave-failure-notice.spec.js index fa04e9f931751b..bece26101456ba 100644 --- a/test/e2e/specs/editor/various/autosave-failure-notice.spec.js +++ b/test/e2e/specs/editor/various/autosave-failure-notice.spec.js @@ -85,7 +85,7 @@ test.describe( 'Autosave failure notice', () => { // WordPress version, so only its start is checked. withDetails( expect.stringMatching( - /^There has been a critical error on this website\.\nLearn more about troubleshooting WordPress\. \(https:\/\/wordpress\.org\/documentation\/article\/faq-troubleshooting\/\)\n\nUncaught Error: Call to undefined function gutenberg_test_autosave_undefined_function\(\) in .+\nStack trace:\n#0 / + /^Error code 500: There has been a critical error on this website\.\nLearn more about troubleshooting WordPress\. \(https:\/\/wordpress\.org\/documentation\/article\/faq-troubleshooting\/\)\n\nUncaught Error: Call to undefined function gutenberg_test_autosave_undefined_function\(\) in .+\nStack trace:\n#0 / ) ), ], @@ -93,14 +93,14 @@ test.describe( 'Autosave failure notice', () => { 'database connection lost', 'database-connection-lost', withDetails( - 'Error establishing a database connection\n\nError 500' + 'Error code 500: Error establishing a database connection' ), ], [ 'error message containing a link', 'link-in-message', withDetails( - 'Blocked. Learn more (https://example.com).\n\nError 403' + 'Error code 403: Blocked. Learn more (https://example.com).' ), ], // TODO: Retry right away. @@ -114,13 +114,15 @@ test.describe( 'Autosave failure notice', () => { [ 'error with a plain text message', 'plain-message', - withDetails( 'Something went wrong on the server.\n\nError 500' ), + withDetails( + 'Error code 500: Something went wrong on the server.' + ), ], [ 'permission denied', 'permission', withDetails( - 'Sorry, you are not allowed to edit this post.\n\nError 403' + 'Error code 403: Sorry, you are not allowed to edit this post.' ), ], // The error's `data` is the database error, so there is no status in it. @@ -129,11 +131,11 @@ test.describe( 'Autosave failure notice', () => { 'database-write', withDetails( 'Could not update post in the database.' ), ], - [ 'HTML error page', 'bad-gateway', withDetails( 'Error 502' ) ], + [ 'HTML error page', 'bad-gateway', withDetails( 'Error code 502' ) ], [ 'too many requests', 'too-many-requests', - withDetails( 'Error 429' ), + withDetails( 'Error code 429' ), ], // The error is printed before the headers, so the status stays 200. // TODO: Show the PHP error the response includes in `data.error`. @@ -157,7 +159,7 @@ test.describe( 'Autosave failure notice', () => { 'maintenance mode', 'maintenance', withDetails( - 'Briefly unavailable for scheduled maintenance. Check back in a minute.\n\nError 503' + 'Error code 503: Briefly unavailable for scheduled maintenance. Check back in a minute.' ), ], // TODO: Report the failure; the autosave fails without a notice. @@ -212,7 +214,7 @@ test.describe( 'Autosave failure notice', () => { await autosaveFailure.autosave(); // TODO: Say the session expired and offer to log in again. expect( await autosaveFailure.getNotices() ).toEqual( [ - withDetails( 'Cookie check failed\n\nError 403' ), + withDetails( 'Error code 403: Cookie check failed' ), ] ); } ); From 5adf35847f6559e9a9b64d2511a39b60dfbe6df6 Mon Sep 17 00:00:00 2001 From: ella Date: Mon, 19 Oct 2026 19:33:09 -0700 Subject: [PATCH 5/6] Add the status to error responses without data Co-Authored-By: Claude Opus 5.5 --- packages/api-fetch/CHANGELOG.md | 2 +- .../api-fetch/src/test/index.jsdom.test.js | 22 +++++++++++++++++++ packages/api-fetch/src/utils/response.ts | 13 ++++++++++- .../various/autosave-failure-notice.spec.js | 7 ++++-- 4 files changed, 40 insertions(+), 4 deletions(-) diff --git a/packages/api-fetch/CHANGELOG.md b/packages/api-fetch/CHANGELOG.md index 1e26320480faf4..836856d9572b20 100644 --- a/packages/api-fetch/CHANGELOG.md +++ b/packages/api-fetch/CHANGELOG.md @@ -8,7 +8,7 @@ ### Enhancements -- Include the response's status as `data.status` in `invalid_json` errors ([#85157](https://github.com/WordPress/gutenberg/pull/85157)). +- 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) diff --git a/packages/api-fetch/src/test/index.jsdom.test.js b/packages/api-fetch/src/test/index.jsdom.test.js index cbbf614266a45c..38eda68cc2283a 100644 --- a/packages/api-fetch/src/test/index.jsdom.test.js +++ b/packages/api-fetch/src/test/index.jsdom.test.js @@ -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', } ); } ); @@ -361,6 +382,7 @@ describe( 'apiFetch', () => { await expect( apiFetch( { path: '/random' } ) ).rejects.toEqual( { code: 'rest_cookie_invalid_nonce', + data: { status: 403 }, } ); } ); diff --git a/packages/api-fetch/src/utils/response.ts b/packages/api-fetch/src/utils/response.ts index ff1d6641a1ff61..9ff3e0716a8ee3 100644 --- a/packages/api-fetch/src/utils/response.ts +++ b/packages/api-fetch/src/utils/response.ts @@ -76,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; } diff --git a/test/e2e/specs/editor/various/autosave-failure-notice.spec.js b/test/e2e/specs/editor/various/autosave-failure-notice.spec.js index bece26101456ba..c23012070927cd 100644 --- a/test/e2e/specs/editor/various/autosave-failure-notice.spec.js +++ b/test/e2e/specs/editor/various/autosave-failure-notice.spec.js @@ -109,8 +109,11 @@ test.describe( 'Autosave failure notice', () => { 'dropped-connection', AUTOSAVE_FAILED_NO_RESPONSE, ], - // TODO: Show the status code. - [ 'error without a message', 'no-message', AUTOSAVE_FAILED ], + [ + 'error without a message', + 'no-message', + withDetails( 'Error code 500' ), + ], [ 'error with a plain text message', 'plain-message', From 5a0d38424ec714f207acf721aa6ada566807bab8 Mon Sep 17 00:00:00 2001 From: ella Date: Mon, 19 Oct 2026 20:35:04 -0700 Subject: [PATCH 6/6] Use one "Error code N." sentence before the message Co-Authored-By: Claude Opus 5.5 --- packages/editor/src/store/actions.js | 21 ++++++++----------- .../src/store/test/actions.jsdom.test.js | 6 +++--- .../various/autosave-failure-notice.spec.js | 20 +++++++++--------- 3 files changed, 22 insertions(+), 25 deletions(-) diff --git a/packages/editor/src/store/actions.js b/packages/editor/src/store/actions.js index b058adfa885116..4e09dc925b92ef 100644 --- a/packages/editor/src/store/actions.js +++ b/packages/editor/src/store/actions.js @@ -418,21 +418,18 @@ export const savePost = ! CLIENT_GENERATED_ERROR_CODES.includes( error.code ) ? getFailureDetails( error.message ) : ''; - let summary = message; - if ( Number.isInteger( status ) && status >= 400 ) { - summary = message + const summary = [ + Number.isInteger( status ) && status >= 400 ? sprintf( - /* translators: 1: HTTP status code, such as 500. 2: Error message. */ - __( 'Error code %1$d: %2$s' ), - status, - message - ) - : sprintf( /* translators: %d: HTTP status code, such as 500. */ - __( 'Error code %d' ), + __( 'Error code %d.' ), status - ); - } + ) + : '', + message, + ] + .filter( Boolean ) + .join( ' ' ); const details = [ summary, typeof phpError === 'string' ? phpError.trim() : '', diff --git a/packages/editor/src/store/test/actions.jsdom.test.js b/packages/editor/src/store/test/actions.jsdom.test.js index c028d7a48636f8..0edc02a8d08eda 100644 --- a/packages/editor/src/store/test/actions.jsdom.test.js +++ b/packages/editor/src/store/test/actions.jsdom.test.js @@ -427,7 +427,7 @@ describe( 'Post actions', () => { }, }, }, - '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', + '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', @@ -436,7 +436,7 @@ describe( 'Post actions', () => { message: 'Server error.', data: { status: 500, error: { message: [ 'unexpected' ] } }, }, - 'Error code 500: Server error.', + 'Error code 500. Server error.', ], [ 'shows the status of a response that is not JSON', @@ -445,7 +445,7 @@ describe( 'Post actions', () => { message: 'The response is not a valid JSON response.', data: { status: 502 }, }, - 'Error code 502', + 'Error code 502.', ], [ 'shows no details for a successful response that is not JSON', diff --git a/test/e2e/specs/editor/various/autosave-failure-notice.spec.js b/test/e2e/specs/editor/various/autosave-failure-notice.spec.js index c23012070927cd..5da62979b95481 100644 --- a/test/e2e/specs/editor/various/autosave-failure-notice.spec.js +++ b/test/e2e/specs/editor/various/autosave-failure-notice.spec.js @@ -85,7 +85,7 @@ test.describe( 'Autosave failure notice', () => { // WordPress version, so only its start is checked. withDetails( expect.stringMatching( - /^Error code 500: There has been a critical error on this website\.\nLearn more about troubleshooting WordPress\. \(https:\/\/wordpress\.org\/documentation\/article\/faq-troubleshooting\/\)\n\nUncaught Error: Call to undefined function gutenberg_test_autosave_undefined_function\(\) in .+\nStack trace:\n#0 / + /^Error code 500\. There has been a critical error on this website\.\nLearn more about troubleshooting WordPress\. \(https:\/\/wordpress\.org\/documentation\/article\/faq-troubleshooting\/\)\n\nUncaught Error: Call to undefined function gutenberg_test_autosave_undefined_function\(\) in .+\nStack trace:\n#0 / ) ), ], @@ -93,14 +93,14 @@ test.describe( 'Autosave failure notice', () => { 'database connection lost', 'database-connection-lost', withDetails( - 'Error code 500: Error establishing a database connection' + 'Error code 500. Error establishing a database connection' ), ], [ 'error message containing a link', 'link-in-message', withDetails( - 'Error code 403: Blocked. Learn more (https://example.com).' + 'Error code 403. Blocked. Learn more (https://example.com).' ), ], // TODO: Retry right away. @@ -112,20 +112,20 @@ test.describe( 'Autosave failure notice', () => { [ 'error without a message', 'no-message', - withDetails( 'Error code 500' ), + withDetails( 'Error code 500.' ), ], [ 'error with a plain text message', 'plain-message', withDetails( - 'Error code 500: Something went wrong on the server.' + 'Error code 500. Something went wrong on the server.' ), ], [ 'permission denied', 'permission', withDetails( - 'Error code 403: Sorry, you are not allowed to edit this post.' + 'Error code 403. Sorry, you are not allowed to edit this post.' ), ], // The error's `data` is the database error, so there is no status in it. @@ -134,11 +134,11 @@ test.describe( 'Autosave failure notice', () => { 'database-write', withDetails( 'Could not update post in the database.' ), ], - [ 'HTML error page', 'bad-gateway', withDetails( 'Error code 502' ) ], + [ 'HTML error page', 'bad-gateway', withDetails( 'Error code 502.' ) ], [ 'too many requests', 'too-many-requests', - withDetails( 'Error code 429' ), + withDetails( 'Error code 429.' ), ], // The error is printed before the headers, so the status stays 200. // TODO: Show the PHP error the response includes in `data.error`. @@ -162,7 +162,7 @@ test.describe( 'Autosave failure notice', () => { 'maintenance mode', 'maintenance', withDetails( - 'Error code 503: Briefly unavailable for scheduled maintenance. Check back in a minute.' + 'Error code 503. Briefly unavailable for scheduled maintenance. Check back in a minute.' ), ], // TODO: Report the failure; the autosave fails without a notice. @@ -217,7 +217,7 @@ test.describe( 'Autosave failure notice', () => { await autosaveFailure.autosave(); // TODO: Say the session expired and offer to log in again. expect( await autosaveFailure.getNotices() ).toEqual( [ - withDetails( 'Error code 403: Cookie check failed' ), + withDetails( 'Error code 403. Cookie check failed' ), ] ); } );