@aws-amplify/storage
Version:
Storage category of aws-amplify
179 lines (177 loc) • 8.2 kB
JavaScript
// 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
;