UNPKG

filestack-js

Version:

Official JavaScript library for Filestack

191 lines (153 loc) 6.5 kB
/* * Copyright (c) 2018 by Filestack * Some rights reserved. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ import Debug from 'debug'; import * as utils from '../utils'; import { AdapterInterface } from './interface'; import { FsRequestOptions, FsResponse, FsHttpMethod } from '../types'; import { FsRequestError, FsRequestErrorCode } from '../error'; import { prepareData, parseResponse, parse as parseHeaders, combineURL } from './../helpers'; const debug = Debug('fs:request:xhr'); const CANCEL_CLEAR = `FsCleanMemory`; export class XhrAdapter implements AdapterInterface { request(config: FsRequestOptions) { // if this option is unspecified set it by default if (typeof config.filestackHeaders === 'undefined') { config.filestackHeaders = true; } config = prepareData(config); config.headers = config.headers || {}; let { data, headers } = config; // if data is type of form let browser to set proper content type if (utils.isFormData(data)) { delete headers['Content-Type']; } let request = new XMLHttpRequest(); if (config.blobResponse) { request.responseType = 'blob'; } // HTTP basic authentication if (config.auth) { if (!config.auth.username || config.auth.username.length === 0 || !config.auth.password || config.auth.password.length === 0) { return Promise.reject(new FsRequestError(`Basic auth: username and password are required ${config.auth}`, config)); } headers.Authorization = 'Basic ' + btoa(unescape(encodeURIComponent(`${config.auth.username}:${config.auth.password}`))); debug('Set request authorization to %s', config.auth.username + config.auth.password); } let url = config.url.trim(); if (!/^http(s)?:\/\//.test(url)) { url = `https://${url}`; } url = combineURL(url, config.params); debug('Starting request to %s with options %O', url, config); request.open(config.method.toUpperCase(), url, true); request.timeout = config.timeout; return new Promise<FsResponse>((resolve, reject) => { let cancelListener; if (config.cancelToken) { cancelListener = (reason) => { /* istanbul ignore next: if request is done cancel token should not throw any error */ if (request) { request.abort(); request = null; } debug('Request canceled by user %s, config: %O', reason, config); return reject(new FsRequestError(`Request aborted. Reason: ${reason}`, config, null, FsRequestErrorCode.ABORTED)); }; config.cancelToken.once('cancel', cancelListener); } request.onreadystatechange = async () => { if (!request || request.readyState !== 4) { return; } if (request.status === 0 && !request.responseURL) { return; } // Prepare the response const responseHeaders = parseHeaders(request.getAllResponseHeaders()); const responseData = request.response; let response: FsResponse = { data: responseData, status: request.status, statusText: request.statusText, headers: responseHeaders, config: config, }; request = null; response = await parseResponse(response); if (500 <= response.status && response.status <= 599) { // server error throw debug('Server error(5xx) - %O', response); return reject(new FsRequestError(`Server error ${url}`, config, response, FsRequestErrorCode.SERVER)); } else if (400 <= response.status && response.status <= 499) { debug('Request error(4xx) - %O', response); return reject(new FsRequestError(`Request error ${url}`, config, response, FsRequestErrorCode.REQUEST)); } // clear cancel token to avoid memory leak if (config.cancelToken) { config.cancelToken.removeListener('cancel', cancelListener); cancelListener = null; } return resolve(response); }; // Handle browser request cancellation (as opposed to a manual cancellation) request.onabort = function handleAbort() { /* istanbul ignore next: just to be sure that abort was not called twice */ if (!request) { return; } request = null; reject(new FsRequestError('Request aborted', config, null, FsRequestErrorCode.ABORTED)); }; // Handle low level network errors request.onerror = function handleError(err) { request = null; debug('Request error! %O', err); reject(new FsRequestError('Network Error', config, null, FsRequestErrorCode.NETWORK)); }; // Handle timeout request.ontimeout = function handleTimeout() { request = null; debug('Request timed out. %O', config); reject(new FsRequestError('Request timeout', config, null, FsRequestErrorCode.TIMEOUT)); }; // Add headers to the request if ('setRequestHeader' in request && headers && Object.keys(headers).length) { for (let key in headers) { if (headers[key] === undefined) { continue; } debug('Set request header %s to %s', key, headers[key]); request.setRequestHeader(key, headers[key]); } } if (typeof config.onProgress === 'function' && [FsHttpMethod.POST, FsHttpMethod.PUT].indexOf(config.method) > -1) { /* istanbul ignore else: else path is just fallback to normal progress event */ if (request.upload) { debug('Bind to upload progress event'); request.upload.addEventListener('progress', config.onProgress); } else { debug('Bind to progress event'); request.addEventListener('progress', config.onProgress); } } if (data === undefined) { data = null; } request.send(data); }); } }