Skip to content

Commit

Permalink
Add an apiFetch middleware to automatically handle media upload failu…
Browse files Browse the repository at this point in the history
…res (#17858)

* Add an apiFetch middleware to automatically handle media upload failures

* Remove the attachement on failures

* Handle errors properly

* limit the media upload middleware to the 500 responses

* Fix the error handling and unit tests

* Api Fetch: Check for 502s and parse uncaught errors in Media Upload middleware.
  • Loading branch information
youknowriad authored and hypest committed Nov 4, 2019
1 parent 70986bd commit 0ffde5a
Show file tree
Hide file tree
Showing 4 changed files with 151 additions and 46 deletions.
3 changes: 2 additions & 1 deletion lib/client-assets.php
Original file line number Diff line number Diff line change
Expand Up @@ -244,7 +244,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' )
),
Expand Down
50 changes: 5 additions & 45 deletions packages/api-fetch/src/index.js
Original file line number Diff line number Diff line change
@@ -1,8 +1,3 @@
/**
* WordPress dependencies
*/
import { __ } from '@wordpress/i18n';

/**
* Internal dependencies
*/
Expand All @@ -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
Expand Down Expand Up @@ -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;
Expand Down Expand Up @@ -179,5 +138,6 @@ apiFetch.createNonceMiddleware = createNonceMiddleware;
apiFetch.createPreloadingMiddleware = createPreloadingMiddleware;
apiFetch.createRootURLMiddleware = createRootURLMiddleware;
apiFetch.fetchAllMiddleware = fetchAllMiddleware;
apiFetch.mediaUploadMiddleware = mediaUploadMiddleware;

export default apiFetch;
74 changes: 74 additions & 0 deletions packages/api-fetch/src/middlewares/media-upload.js
Original file line number Diff line number Diff line change
@@ -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;
70 changes: 70 additions & 0 deletions packages/api-fetch/src/utils/response.js
Original file line number Diff line number Diff line change
@@ -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;
} );
}

0 comments on commit 0ffde5a

Please sign in to comment.