UNPKG

@aws-amplify/storage

Version:

Storage category of aws-amplify

179 lines (177 loc) • 8.2 kB
'use strict'; // Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. // SPDX-License-Identifier: Apache-2.0 Object.defineProperty(exports, "__esModule", { value: true }); exports.xhrTransferHandler = void 0; const aws_client_utils_1 = require("@aws-amplify/core/internals/aws-client-utils"); const core_1 = require("@aws-amplify/core"); const constants_1 = require("./constants"); const CanceledError_1 = require("../../../../../errors/CanceledError"); const logger = new core_1.ConsoleLogger('xhr-http-handler'); /** * Base transfer handler implementation using XMLHttpRequest to support upload and download progress events. * * @param request - The request object. * @param options - The request options. * @returns A promise that will be resolved with the response object. * * @internal */ const xhrTransferHandler = (request, options) => { const { url, method, headers, body } = request; const { onDownloadProgress, onUploadProgress, responseType, abortSignal } = options; return new Promise((resolve, reject) => { let xhr = new XMLHttpRequest(); xhr.open(method.toUpperCase(), url.toString()); Object.entries(headers) .filter(([header]) => !FORBIDDEN_HEADERS.includes(header)) .forEach(([header, value]) => { xhr.setRequestHeader(header, value); }); xhr.responseType = responseType; if (onDownloadProgress) { xhr.addEventListener('progress', event => { onDownloadProgress(convertToTransferProgressEvent(event)); logger.debug(event); }); } if (onUploadProgress) { xhr.upload.addEventListener('progress', event => { onUploadProgress(convertToTransferProgressEvent(event)); logger.debug(event); }); } xhr.addEventListener('error', () => { const networkError = buildHandlerError(constants_1.NETWORK_ERROR_MESSAGE, constants_1.NETWORK_ERROR_CODE); logger.error(constants_1.NETWORK_ERROR_MESSAGE); reject(networkError); xhr = null; // clean up request }); // Handle browser request cancellation (as opposed to a manual cancellation) xhr.addEventListener('abort', () => { // The abort event can be triggered after the error or load event. So we need to check if the xhr is null. // When request is aborted by AbortSignal, the promise is rejected in the abortSignal's 'abort' event listener. if (!xhr || abortSignal?.aborted) return; // Handle abort request caused by browser instead of AbortController // see: https://github.com/axios/axios/issues/537 const error = buildHandlerError(constants_1.ABORT_ERROR_MESSAGE, constants_1.ABORT_ERROR_CODE); logger.error(constants_1.ABORT_ERROR_MESSAGE); reject(error); xhr = null; // clean up request }); // Skip handling timeout error since we don't have a timeout xhr.addEventListener('readystatechange', () => { if (!xhr || xhr.readyState !== xhr.DONE) { return; } const onloadend = () => { // The load event is triggered after the error/abort/load event. So we need to check if the xhr is null. if (!xhr) return; const responseHeaders = convertResponseHeaders(xhr.getAllResponseHeaders()); const responseType = xhr.responseType; const responseBlob = xhr.response; const responseText = responseType === 'text' ? xhr.responseText : ''; const bodyMixIn = { blob: () => Promise.resolve(responseBlob), text: (0, aws_client_utils_1.withMemoization)(() => responseType === 'blob' ? readBlobAsText(responseBlob) : Promise.resolve(responseText)), json: () => Promise.reject( // S3 does not support JSON response. So fail-fast here with nicer error message. new Error('Parsing response to JSON is not implemented. Please use response.text() instead.')), }; const response = { statusCode: xhr.status, headers: responseHeaders, // The xhr.responseType is only set to 'blob' for streaming binary S3 object data. The streaming data is // exposed via public interface of Storage.get(). So we need to return the response as a Blob object for // backward compatibility. In other cases, the response payload is only used internally, we return it is // {@link ResponseBodyMixin} body: (xhr.responseType === 'blob' ? Object.assign(responseBlob, bodyMixIn) : bodyMixIn), }; resolve(response); xhr = null; // clean up request }; // readystate handler is calling before onerror or ontimeout handlers, // so we should call onloadend on the next 'tick' // @see https://github.com/axios/axios/blob/9588fcdec8aca45c3ba2f7968988a5d03f23168c/lib/adapters/xhr.js#L98-L99 setTimeout(onloadend); }); if (abortSignal) { const onCanceled = () => { // The abort event is triggered after the error or load event. So we need to check if the xhr is null. if (!xhr) { return; } const canceledError = new CanceledError_1.CanceledError({ name: constants_1.CANCELED_ERROR_CODE, message: constants_1.CANCELED_ERROR_MESSAGE, }); reject(canceledError); xhr.abort(); xhr = null; }; abortSignal.aborted ? onCanceled() : abortSignal.addEventListener('abort', onCanceled); } if (typeof ReadableStream === 'function' && body instanceof ReadableStream) { // This does not matter as previous implementation uses Axios which does not support ReadableStream anyway. throw new Error('ReadableStream request payload is not supported.'); } xhr.send(body ?? null); }); }; exports.xhrTransferHandler = xhrTransferHandler; const convertToTransferProgressEvent = (event) => ({ transferredBytes: event.loaded, totalBytes: event.lengthComputable ? event.total : undefined, }); const buildHandlerError = (message, name) => { const error = new Error(message); error.name = name; return error; }; /** * Convert xhr.getAllResponseHeaders() string to a Record<string, string>. Note that modern browser already returns * header names in lowercase. * @param xhrHeaders - string of headers returned from xhr.getAllResponseHeaders() */ const convertResponseHeaders = (xhrHeaders) => { if (!xhrHeaders) { return {}; } return xhrHeaders .split('\r\n') .reduce((headerMap, line) => { const parts = line.split(': '); const header = parts.shift(); const value = parts.join(': '); headerMap[header.toLowerCase()] = value; return headerMap; }, {}); }; const readBlobAsText = (blob) => { const reader = new FileReader(); return new Promise((resolve, reject) => { reader.onloadend = () => { if (reader.readyState !== FileReader.DONE) { return; } resolve(reader.result); }; reader.onerror = () => { reject(reader.error); }; reader.readAsText(blob); }); }; // To add more forbidden headers as found set by S3. Intentionally NOT list all of them here to save bundle size. // https://developer.mozilla.org/en-US/docs/Glossary/Forbidden_header_name const FORBIDDEN_HEADERS = ['host']; //# sourceMappingURL=xhrTransferHandler.js.map