javascript: make Uploader always throw the same kind of errors
A DirectUpload may fail for several reasons, and return many types of errors (string, xhr response, Error objects, etc). For convenience, wrap all these errors in a FileUploadError object. - It makes easier for clients of the Uploader class to handle errors; - It allows to propagate the error code and failure responsability.
This commit is contained in:
parent
d8f3b86b0e
commit
432967bd76
5 changed files with 92 additions and 53 deletions
|
@ -18,6 +18,8 @@ export default class AutoUploadController {
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Create, upload and attach the file.
|
||||||
|
// On failure, display an error message and throw a FileUploadError.
|
||||||
async start() {
|
async start() {
|
||||||
try {
|
try {
|
||||||
this._begin();
|
this._begin();
|
||||||
|
@ -55,36 +57,15 @@ export default class AutoUploadController {
|
||||||
this.input.disabled = false;
|
this.input.disabled = false;
|
||||||
}
|
}
|
||||||
|
|
||||||
_isError422(error) {
|
|
||||||
// Ajax errors have an xhr attribute
|
|
||||||
if (error && error.xhr && error.xhr.status == 422) return true;
|
|
||||||
// Rails DirectUpload errors are returned as a String, e.g. 'Error creating Blob for "Demain.txt". Status: 422'
|
|
||||||
if (error && error.toString().includes('422')) return true;
|
|
||||||
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
_messageFromError(error) {
|
_messageFromError(error) {
|
||||||
let allowRetry = !this._isError422(error);
|
let message = error.message || error.toString();
|
||||||
|
let canRetry = error.status && error.status != 422;
|
||||||
|
|
||||||
|
|
||||||
if (
|
|
||||||
error.xhr &&
|
|
||||||
error.xhr.status == 422 &&
|
|
||||||
error.response &&
|
|
||||||
error.response.errors &&
|
|
||||||
error.response.errors[0]
|
|
||||||
) {
|
|
||||||
return {
|
return {
|
||||||
title: error.response.errors[0],
|
title: 'Le fichier n’a pas pu être envoyé.',
|
||||||
description: '',
|
description: message,
|
||||||
retry: allowRetry
|
retry: canRetry
|
||||||
};
|
|
||||||
} else {
|
|
||||||
return {
|
|
||||||
title: 'Une erreur s’est produite pendant l’envoi du fichier.',
|
|
||||||
description: error.message || error.toString(),
|
|
||||||
retry: allowRetry
|
|
||||||
};
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -1,22 +0,0 @@
|
||||||
// Convert an error message returned by DirectUpload to a proper error object.
|
|
||||||
//
|
|
||||||
// This function has two goals:
|
|
||||||
// 1. Remove the file name from the DirectUpload error message
|
|
||||||
// (because the filename confuses Sentry error grouping)
|
|
||||||
// 2. Create each kind of error on a different line
|
|
||||||
// (so that Sentry knows they are different kind of errors, from
|
|
||||||
// the line they were created.)
|
|
||||||
export default function errorFromDirectUploadMessage(message) {
|
|
||||||
let matches = message.match(/ Status: [0-9]{1,3}/);
|
|
||||||
let status = (matches && matches[0]) || '';
|
|
||||||
|
|
||||||
if (message.includes('Error creating')) {
|
|
||||||
return new Error('Error creating file.' + status);
|
|
||||||
} else if (message.includes('Error storing')) {
|
|
||||||
return new Error('Error storing file.' + status);
|
|
||||||
} else if (message.includes('Error reading')) {
|
|
||||||
return new Error('Error reading file.' + status);
|
|
||||||
} else {
|
|
||||||
return new Error(message);
|
|
||||||
}
|
|
||||||
}
|
|
67
app/javascript/shared/activestorage/file-upload-error.js
Normal file
67
app/javascript/shared/activestorage/file-upload-error.js
Normal file
|
@ -0,0 +1,67 @@
|
||||||
|
// Error while reading the file client-side
|
||||||
|
export const ERROR_CODE_READ = 'file-upload-read-error';
|
||||||
|
// Error while creating the empty blob on the server
|
||||||
|
export const ERROR_CODE_CREATE = 'file-upload-create-error';
|
||||||
|
// Error while uploading the blob content
|
||||||
|
export const ERROR_CODE_STORE = 'file-upload-store-error';
|
||||||
|
// Error while attaching the blob to the record
|
||||||
|
export const ERROR_CODE_ATTACH = 'file-upload-attach-error';
|
||||||
|
|
||||||
|
// Failure on the client side (syntax error, error reading file, etc.)
|
||||||
|
export const FAILURE_CLIENT = 'file-upload-failure-client';
|
||||||
|
// Failure on the server side (typically non-200 response)
|
||||||
|
export const FAILURE_SERVER = 'file-upload-failure-server';
|
||||||
|
// Failure during the transfert (request aborted, connection lost, etc)
|
||||||
|
export const FAILURE_CONNECTIVITY = 'file-upload-failure-connectivity';
|
||||||
|
|
||||||
|
/**
|
||||||
|
Represent an error during a file upload.
|
||||||
|
*/
|
||||||
|
export default class FileUploadError extends Error {
|
||||||
|
constructor(message, status, code) {
|
||||||
|
super(message);
|
||||||
|
this.name = 'FileUploadError';
|
||||||
|
this.status = status;
|
||||||
|
this.code = code;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
Return the component responsible of the error (client, server or connectivity).
|
||||||
|
See FAILURE_* constants for values.
|
||||||
|
*/
|
||||||
|
get failureReason() {
|
||||||
|
let isNetworkError = this.code != ERROR_CODE_READ;
|
||||||
|
|
||||||
|
if (isNetworkError && this.status != 0) {
|
||||||
|
return FAILURE_SERVER;
|
||||||
|
} else if (isNetworkError && this.status == 0) {
|
||||||
|
return FAILURE_CONNECTIVITY;
|
||||||
|
} else {
|
||||||
|
return FAILURE_CLIENT;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Convert an error message returned by DirectUpload to a proper error object.
|
||||||
|
//
|
||||||
|
// This function has two goals:
|
||||||
|
// 1. Remove the file name from the DirectUpload error message
|
||||||
|
// (because the filename confuses Sentry error grouping)
|
||||||
|
// 2. Create each kind of error on a different line
|
||||||
|
// (so that Sentry knows they are different kind of errors, from
|
||||||
|
// the line they were created.)
|
||||||
|
export function errorFromDirectUploadMessage(message) {
|
||||||
|
let matches = message.match(/ Status: [0-9]{1,3}/);
|
||||||
|
let status = (matches && parseInt(matches[0], 10)) || undefined;
|
||||||
|
|
||||||
|
// prettier-ignore
|
||||||
|
if (message.includes('Error reading')) {
|
||||||
|
return new FileUploadError('Error reading file.', status, ERROR_CODE_READ);
|
||||||
|
} else if (message.includes('Error creating')) {
|
||||||
|
return new FileUploadError('Error creating file.', status, ERROR_CODE_CREATE);
|
||||||
|
} else if (message.includes('Error storing')) {
|
||||||
|
return new FileUploadError('Error storing file.', status, ERROR_CODE_STORE);
|
||||||
|
} else {
|
||||||
|
return new FileUploadError(message, status, undefined);
|
||||||
|
}
|
||||||
|
}
|
|
@ -1,5 +1,5 @@
|
||||||
import ProgressBar from './progress-bar';
|
import ProgressBar from './progress-bar';
|
||||||
import errorFromDirectUploadMessage from './errors';
|
import { errorFromDirectUploadMessage } from './file-upload-error';
|
||||||
import { fire } from '@utils';
|
import { fire } from '@utils';
|
||||||
|
|
||||||
const INITIALIZE_EVENT = 'direct-upload:initialize';
|
const INITIALIZE_EVENT = 'direct-upload:initialize';
|
||||||
|
|
|
@ -1,7 +1,7 @@
|
||||||
import { DirectUpload } from '@rails/activestorage';
|
import { DirectUpload } from '@rails/activestorage';
|
||||||
import { ajax } from '@utils';
|
import { ajax } from '@utils';
|
||||||
import ProgressBar from './progress-bar';
|
import ProgressBar from './progress-bar';
|
||||||
import errorFromDirectUploadMessage from './errors';
|
import FileUploadError, { errorFromDirectUploadMessage, ERROR_CODE_ATTACH } from './file-upload-error';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
Uploader class is a delegate for DirectUpload instance
|
Uploader class is a delegate for DirectUpload instance
|
||||||
|
@ -17,6 +17,7 @@ export default class Uploader {
|
||||||
/**
|
/**
|
||||||
Upload (and optionally attach) the file.
|
Upload (and optionally attach) the file.
|
||||||
Returns the blob signed id on success.
|
Returns the blob signed id on success.
|
||||||
|
Throws a FileUploadError on failure.
|
||||||
*/
|
*/
|
||||||
async start() {
|
async start() {
|
||||||
this.progressBar.start();
|
this.progressBar.start();
|
||||||
|
@ -40,6 +41,7 @@ export default class Uploader {
|
||||||
|
|
||||||
/**
|
/**
|
||||||
Upload the file using the DirectUpload instance, and return the blob signed_id.
|
Upload the file using the DirectUpload instance, and return the blob signed_id.
|
||||||
|
Throws a FileUploadError on failure.
|
||||||
*/
|
*/
|
||||||
async _upload() {
|
async _upload() {
|
||||||
return new Promise((resolve, reject) => {
|
return new Promise((resolve, reject) => {
|
||||||
|
@ -56,6 +58,8 @@ export default class Uploader {
|
||||||
|
|
||||||
/**
|
/**
|
||||||
Attach the file by sending a POST request to the autoAttachUrl.
|
Attach the file by sending a POST request to the autoAttachUrl.
|
||||||
|
Throws a FileUploadError on failure (containing the first validation
|
||||||
|
error message, if any).
|
||||||
*/
|
*/
|
||||||
async _attach(blobSignedId) {
|
async _attach(blobSignedId) {
|
||||||
const attachmentRequest = {
|
const attachmentRequest = {
|
||||||
|
@ -64,7 +68,16 @@ export default class Uploader {
|
||||||
data: `blob_signed_id=${blobSignedId}`
|
data: `blob_signed_id=${blobSignedId}`
|
||||||
};
|
};
|
||||||
|
|
||||||
|
try {
|
||||||
await ajax(attachmentRequest);
|
await ajax(attachmentRequest);
|
||||||
|
} catch (e) {
|
||||||
|
let message = e.response && e.response.errors && e.response.errors[0];
|
||||||
|
throw new FileUploadError(
|
||||||
|
message || 'Error attaching file.',
|
||||||
|
e.xhr.status,
|
||||||
|
ERROR_CODE_ATTACH
|
||||||
|
);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
uploadRequestDidProgress(event) {
|
uploadRequestDidProgress(event) {
|
||||||
|
|
Loading…
Reference in a new issue