diff --git a/lib/client-assets.php b/lib/client-assets.php index 6ad90f97fc8f52..b34eccebf8a977 100644 --- a/lib/client-assets.php +++ b/lib/client-assets.php @@ -249,7 +249,8 @@ function gutenberg_register_scripts_and_styles() { sprintf( 'wp.apiFetch.nonceMiddleware = wp.apiFetch.createNonceMiddleware( "%s" );' . 'wp.apiFetch.use( wp.apiFetch.nonceMiddleware );' . - 'wp.apiFetch.nonceEndpoint = "%s";', + 'wp.apiFetch.nonceEndpoint = "%s";' . + 'wp.apiFetch.use( wp.apiFetch.mediaUploadMiddleware );', ( wp_installing() && ! is_multisite() ) ? '' : wp_create_nonce( 'wp_rest' ), admin_url( 'admin-ajax.php?action=gutenberg_rest_nonce' ) ), diff --git a/packages/api-fetch/src/index.js b/packages/api-fetch/src/index.js index 7535d2117b2f24..dc7dda91fa1e60 100644 --- a/packages/api-fetch/src/index.js +++ b/packages/api-fetch/src/index.js @@ -1,8 +1,3 @@ -/** - * WordPress dependencies - */ -import { __ } from '@wordpress/i18n'; - /** * Internal dependencies */ @@ -13,6 +8,8 @@ import fetchAllMiddleware from './middlewares/fetch-all-middleware'; import namespaceEndpointMiddleware from './middlewares/namespace-endpoint'; import httpV1Middleware from './middlewares/http-v1'; import userLocaleMiddleware from './middlewares/user-locale'; +import mediaUploadMiddleware from './middlewares/media-upload'; +import { parseResponseAndNormalizeError, parseAndThrowError } from './utils/response'; /** * Default set of header values which should be sent with every request unless @@ -80,48 +77,10 @@ const defaultFetchHandler = ( nextOptions ) => { } ); - const parseResponse = ( response ) => { - if ( parse ) { - if ( response.status === 204 ) { - return null; - } - - return response.json ? response.json() : Promise.reject( response ); - } - - return response; - }; - return responsePromise .then( checkStatus ) - .then( parseResponse ) - .catch( ( response ) => { - if ( ! parse ) { - throw response; - } - - const invalidJsonError = { - code: 'invalid_json', - message: __( 'The response is not a valid JSON response.' ), - }; - - if ( ! response || ! response.json ) { - throw invalidJsonError; - } - - return response.json() - .catch( () => { - throw invalidJsonError; - } ) - .then( ( error ) => { - const unknownError = { - code: 'unknown_error', - message: __( 'An unknown error occurred.' ), - }; - - throw error || unknownError; - } ); - } ); + .catch( ( response ) => parseAndThrowError( response, parse ) ) + .then( ( response ) => parseResponseAndNormalizeError( response, parse ) ); }; let fetchHandler = defaultFetchHandler; @@ -179,5 +138,6 @@ apiFetch.createNonceMiddleware = createNonceMiddleware; apiFetch.createPreloadingMiddleware = createPreloadingMiddleware; apiFetch.createRootURLMiddleware = createRootURLMiddleware; apiFetch.fetchAllMiddleware = fetchAllMiddleware; +apiFetch.mediaUploadMiddleware = mediaUploadMiddleware; export default apiFetch; diff --git a/packages/api-fetch/src/middlewares/media-upload.js b/packages/api-fetch/src/middlewares/media-upload.js new file mode 100644 index 00000000000000..b7a14d6b5b050a --- /dev/null +++ b/packages/api-fetch/src/middlewares/media-upload.js @@ -0,0 +1,74 @@ +/** + * WordPress dependencies + */ +import { __ } from '@wordpress/i18n'; + +/** + * Internal dependencies + */ +import { + parseAndThrowError, + parseResponseAndNormalizeError, +} from '../utils/response'; + +/** + * Middleware handling media upload failures and retries. + * + * @param {Object} options Fetch options. + * @param {Function} next [description] + * + * @return {*} The evaluated result of the remaining middleware chain. + */ +function mediaUploadMiddleware( options, next ) { + const isMediaUploadRequest = + ( options.path && options.path.indexOf( '/wp/v2/media' ) !== -1 ) || + ( options.url && options.url.indexOf( '/wp/v2/media' ) !== -1 ); + + if ( ! isMediaUploadRequest ) { + return next( options, next ); + } + let retries = 0; + const maxRetries = 5; + + const postProcess = ( attachmentId ) => { + retries++; + return next( { + path: `/wp/v2/media/${ attachmentId }/post-process`, + method: 'POST', + data: { action: 'create-image-subsizes' }, + parse: false, + } ) + .catch( () => { + if ( retries < maxRetries ) { + return postProcess( attachmentId ); + } + next( { + path: `/wp/v2/media/${ attachmentId }?force=true`, + method: 'DELETE', + } ); + + return Promise.reject(); + } ); + }; + + return next( { ...options, parse: false } ) + .catch( ( response ) => { + const attachmentId = response.headers.get( 'x-wp-upload-attachment-id' ); + if ( ( response.status === 500 || response.status === 502 ) && attachmentId ) { + return postProcess( attachmentId ).catch( () => { + if ( options.parse !== false ) { + return Promise.reject( { + code: 'post_process', + message: __( 'Media upload failed. If this is a photo or a large image, please scale it down and try again.' ), + } ); + } + + return Promise.reject( response ); + } ); + } + return parseAndThrowError( response, options.parse ); + } ) + .then( ( response ) => parseResponseAndNormalizeError( response, options.parse ) ); +} + +export default mediaUploadMiddleware; diff --git a/packages/api-fetch/src/utils/response.js b/packages/api-fetch/src/utils/response.js new file mode 100644 index 00000000000000..bb7b79ce2f8254 --- /dev/null +++ b/packages/api-fetch/src/utils/response.js @@ -0,0 +1,70 @@ +/** + * WordPress dependencies + */ +import { __ } from '@wordpress/i18n'; + +/** + * Parses the apiFetch response. + * + * @param {Response} response + * @param {boolean} shouldParseResponse + * + * @return {Promise} Parsed response + */ +const parseResponse = ( response, shouldParseResponse = true ) => { + if ( shouldParseResponse ) { + if ( response.status === 204 ) { + return null; + } + + return response.json ? response.json() : Promise.reject( response ); + } + + return response; +}; + +const parseJsonAndNormalizeError = ( response ) => { + const invalidJsonError = { + code: 'invalid_json', + message: __( 'The response is not a valid JSON response.' ), + }; + + if ( ! response || ! response.json ) { + throw invalidJsonError; + } + + return response.json() + .catch( () => { + throw invalidJsonError; + } ); +}; + +/** + * Parses the apiFetch response properly and normalize response errors. + * + * @param {Response} response + * @param {boolean} shouldParseResponse + * + * @return {Promise} Parsed response. + */ +export const parseResponseAndNormalizeError = ( response, shouldParseResponse = true ) => { + return Promise.resolve( parseResponse( response, shouldParseResponse ) ) + .catch( ( res ) => parseAndThrowError( res, shouldParseResponse ) ); +}; + +export function parseAndThrowError( response, shouldParseResponse = true ) { + if ( ! shouldParseResponse ) { + throw response; + } + + return parseJsonAndNormalizeError( response ) + .then( ( error ) => { + const unknownError = { + code: 'unknown_error', + message: __( 'An unknown error occurred.' ), + }; + + throw error || unknownError; + } ); +} +