@esri/arcgis-rest-request
Version:
Common methods and utilities for @esri/arcgis-rest-js packages.
1,178 lines (1,157 loc) • 149 kB
JavaScript
/* @preserve
* @esri/arcgis-rest-request - v4.7.1 - Apache-2.0
* Copyright (c) 2017-2025 Esri, Inc.
* Mon Jul 28 2025 20:35:51 GMT+0000 (Coordinated Universal Time)
*/
/******************************************************************************
Copyright (c) Microsoft Corporation.
Permission to use, copy, modify, and/or distribute this software for any
purpose with or without fee is hereby granted.
THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES WITH
REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY
AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY SPECIAL, DIRECT,
INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING FROM
LOSS OF USE, DATA OR PROFITS, WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE OR
OTHER TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR
PERFORMANCE OF THIS SOFTWARE.
***************************************************************************** */
function __rest(s, e) {
var t = {};
for (var p in s) if (Object.prototype.hasOwnProperty.call(s, p) && e.indexOf(p) < 0)
t[p] = s[p];
if (s != null && typeof Object.getOwnPropertySymbols === "function")
for (var i = 0, p = Object.getOwnPropertySymbols(s); i < p.length; i++) {
if (e.indexOf(p[i]) < 0 && Object.prototype.propertyIsEnumerable.call(s, p[i]))
t[p[i]] = s[p[i]];
}
return t;
}
/* Copyright (c) 2017 Environmental Systems Research Institute, Inc.
* Apache-2.0 */
/**
* Checks parameters to see if we should use FormData to send the request
* @param params The object whose keys will be encoded.
* @return A boolean indicating if FormData will be required.
*/
function requiresFormData(params) {
return Object.keys(params).some((key) => {
let value = params[key];
if (!value) {
return false;
}
if (value && value.toParam) {
value = value.toParam();
}
const type = value.constructor.name;
switch (type) {
case "Array":
return false;
case "Object":
return false;
case "Date":
return false;
case "Function":
return false;
case "Boolean":
return false;
case "String":
return false;
case "Number":
return false;
default:
return true;
}
});
}
/**
* Converts parameters to the proper representation to send to the ArcGIS REST API.
* @param params The object whose keys will be encoded.
* @return A new object with properly encoded values.
*/
function processParams(params) {
const newParams = {};
Object.keys(params).forEach((key) => {
var _a, _b;
let param = params[key];
if (param && param.toParam) {
param = param.toParam();
}
if (!param &&
param !== 0 &&
typeof param !== "boolean" &&
typeof param !== "string") {
return;
}
const type = param.constructor.name;
let value;
// properly encodes objects, arrays and dates for arcgis.com and other services.
// ported from https://github.com/Esri/esri-leaflet/blob/master/src/Request.js#L22-L30
// also see https://github.com/Esri/arcgis-rest-js/issues/18:
// null, undefined, function are excluded. If you want to send an empty key you need to send an empty string "".
switch (type) {
case "Array":
// Based on the first element of the array, classify array as an array of arrays, an array of objects
// to be stringified, or an array of non-objects to be comma-separated
// eslint-disable-next-line no-case-declarations
const firstElementType = (_b = (_a = param[0]) === null || _a === void 0 ? void 0 : _a.constructor) === null || _b === void 0 ? void 0 : _b.name;
value =
firstElementType === "Array"
? param // pass thru array of arrays
: firstElementType === "Object"
? JSON.stringify(param) // stringify array of objects
: param.join(","); // join other types of array elements
break;
case "Object":
value = JSON.stringify(param);
break;
case "Date":
value = param.valueOf();
break;
case "Function":
value = null;
break;
case "Boolean":
value = param + "";
break;
default:
value = param;
break;
}
if (value ||
value === 0 ||
typeof value === "string" ||
Array.isArray(value)) {
newParams[key] = value;
}
});
return newParams;
}
/* Copyright (c) 2017 Environmental Systems Research Institute, Inc.
* Apache-2.0 */
/**
* Encodes keys and parameters for use in a URL's query string.
*
* @param key Parameter's key
* @param value Parameter's value
* @returns Query string with key and value pairs separated by "&"
*/
function encodeParam(key, value) {
// For array of arrays, repeat key=value for each element of containing array
if (Array.isArray(value) && value[0] && Array.isArray(value[0])) {
return value
.map((arrayElem) => encodeParam(key, arrayElem))
.join("&");
}
return encodeURIComponent(key) + "=" + encodeURIComponent(value);
}
/**
* Encodes the passed object as a query string.
*
* @param params An object to be encoded.
* @returns An encoded query string.
*/
function encodeQueryString(params) {
const newParams = processParams(params);
return Object.keys(newParams)
.map((key) => {
return encodeParam(key, newParams[key]);
})
.join("&");
}
const FormData = globalThis.FormData;
const File = globalThis.File;
const Blob$1 = globalThis.Blob;
/* Copyright (c) 2017 Environmental Systems Research Institute, Inc.
* Apache-2.0 */
/**
* Encodes parameters in a [FormData](https://developer.mozilla.org/en-US/docs/Web/API/FormData) object in browsers or in a [FormData](https://github.com/form-data/form-data) in Node.js
*
* @param params An object to be encoded.
* @returns The complete [FormData](https://developer.mozilla.org/en-US/docs/Web/API/FormData) object.
*/
function encodeFormData(params, forceFormData) {
// see https://github.com/Esri/arcgis-rest-js/issues/499 for more info.
const useFormData = requiresFormData(params) || forceFormData;
const newParams = processParams(params);
if (useFormData) {
const formData = new FormData();
Object.keys(newParams).forEach((key) => {
if (typeof Blob !== "undefined" && newParams[key] instanceof Blob) {
/* To name the Blob:
1. look to an alternate request parameter called 'fileName'
2. see if 'name' has been tacked onto the Blob manually
3. if all else fails, use the request parameter
*/
const filename = newParams["fileName"] || newParams[key].name || key;
formData.append(key, newParams[key], filename);
}
else {
formData.append(key, newParams[key]);
}
});
return formData;
}
else {
return encodeQueryString(params);
}
}
/* Copyright (c) 2017 Environmental Systems Research Institute, Inc.
* Apache-2.0 */
/**
* This represents a generic error from an ArcGIS endpoint. There will be details about the error in the {@linkcode ArcGISRequestError.message}, {@linkcode ArcGISRequestError.originalMessage} properties on the error. You
* can also access the original server response at {@linkcode ArcGISRequestError.response} which may have additional details.
*
* ```js
* request(someUrl, someOptions).catch(e => {
* if(e.name === "ArcGISRequestError") {
* console.log("Something went wrong with the request:", e);
* console.log("Full server response", e.response);
* }
* })
* ```
*/
class ArcGISRequestError extends Error {
/**
* Create a new `ArcGISRequestError` object.
*
* @param message - The error message from the API
* @param code - The error code from the API
* @param response - The original response from the API that caused the error
* @param url - The original url of the request
* @param options - The original options and parameters of the request
*/
constructor(message, code, response, url, options) {
// 'Error' breaks prototype chain here
super(message);
// restore prototype chain, see https://stackoverflow.com/questions/41102060/typescript-extending-error-class
// we don't need to check for Object.setPrototypeOf as in the answers because we are ES2017 now.
// Also see https://github.com/Microsoft/TypeScript-wiki/blob/main/Breaking-Changes.md#extending-built-ins-like-error-array-and-map-may-no-longer-work
// and https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Error#custom_error_types
const actualProto = new.target.prototype;
Object.setPrototypeOf(this, actualProto);
message = message || "UNKNOWN_ERROR";
code = code || "UNKNOWN_ERROR_CODE";
this.name = "ArcGISRequestError";
this.message =
code === "UNKNOWN_ERROR_CODE" ? message : `${code}: ${message}`;
this.originalMessage = message;
this.code = code;
this.response = response;
this.url = url;
this.options = options;
}
}
/* istanbul ignore file */
// Note: currently this is all internal to the package, and we are not exposing
// anything that a user can set... but we need all this to be able to ensure
// that multiple instances of the package can share the same config.
/**
* The default config for the request module. This is used to store
* the no-cors domains and pending requests.
*/
const DEFAULT_ARCGIS_REQUEST_CONFIG = {
noCorsDomains: [],
crossOriginNoCorsDomains: {},
pendingNoCorsRequests: {}
};
const GLOBAL_VARIABLE_NAME = "ARCGIS_REST_JS_NO_CORS";
// Set the global variable to the default config if it is not aleady defined
// This is done to ensure that all instances of rest-request work with a single
// instance of the config
if (!globalThis[GLOBAL_VARIABLE_NAME]) {
globalThis[GLOBAL_VARIABLE_NAME] = Object.assign({}, DEFAULT_ARCGIS_REQUEST_CONFIG);
}
// export the settings as immutable consts that read from the global config
const requestConfig = globalThis[GLOBAL_VARIABLE_NAME];
/**
* Send a no-cors request to the passed uri. This is used to pick up
* a cookie from a 3rd party server to meet a requirement of some authentication
* flows.
* @param url
* @returns
*/
function sendNoCorsRequest(url) {
// drop any query params, other than f=json
const urlObj = new URL(url);
url = urlObj.origin + urlObj.pathname;
if (urlObj.search.includes("f=json")) {
url += "?f=json";
}
const origin = urlObj.origin;
// If we have already sent a no-cors request to this url, return the promise
// so we don't send multiple requests
if (requestConfig.pendingNoCorsRequests[origin]) {
return requestConfig.pendingNoCorsRequests[origin];
}
// Make the request and add to the cache
requestConfig.pendingNoCorsRequests[origin] = fetch(url, {
mode: "no-cors",
credentials: "include",
cache: "no-store"
})
.then((response) => {
// Add to the list of cross-origin no-cors domains
// if the domain is not already in the list
if (requestConfig.noCorsDomains.indexOf(origin) === -1) {
requestConfig.noCorsDomains.push(origin);
}
// Hold the timestamp of this request so we can decide when to
// send another request to this domain
requestConfig.crossOriginNoCorsDomains[origin.toLowerCase()] = Date.now();
// Remove the pending request from the cache
delete requestConfig.pendingNoCorsRequests[origin];
// Due to limitations of fetchMock at the version of the tooling
// in this project, we can't mock the response type of a no-cors request
// and thus we can't test this. So we are going to comment this out
// and leave it in place for now. If we need to test this, we can
// update the tooling to a version that supports this. Also
// JS SDK does not do this check, so we are going to leave it out for now.
// ================================================================
// no-cors requests are opaque to javascript
// and thus will always return a response with a type of "opaque"
// if (response.type === "opaque") {
// return Promise.resolve();
// } else {
// // Not sure if this is possible, but since we have a check above
// // lets handle the else case
// return Promise.reject(
// new Error(`no-cors request to ${origin} not opaque`)
// );
// }
// ================================================================
})
.catch((e) => {
// Not sure this is necessary, but if the request fails
// we should remove it from the pending requests
// and return a rejected promise with some information
delete requestConfig.pendingNoCorsRequests[origin];
return Promise.reject(new Error(`no-cors request to ${origin} failed`));
});
// return the promise
return requestConfig.pendingNoCorsRequests[origin];
}
/**
* Allow us to get the no-cors domains that are registered
* so we can pass them into the identity manager
* @returns
*/
function getRegisteredNoCorsDomains() {
// return the no-cors domains
return requestConfig.noCorsDomains;
}
/**
* Register the domains that are allowed to be used in no-cors requests
* This is called by `request` when the portal/self response is intercepted
* and the `.authorizedCrossOriginNoCorsDomains` property is set.
* @param authorizedCrossOriginNoCorsDomains
*/
function registerNoCorsDomains(authorizedCrossOriginNoCorsDomains) {
// register the domains
authorizedCrossOriginNoCorsDomains.forEach((domain) => {
// ensure domain is lower case and ensure protocol is included
domain = domain.toLowerCase();
if (/^https?:\/\//.test(domain)) {
addNoCorsDomain(domain);
}
else {
// no protocol present, so add http and https
addNoCorsDomain("http://" + domain);
addNoCorsDomain("https://" + domain);
}
});
}
/**
* Ensure we don't get duplicate domains in the no-cors domains list
* @param domain
*/
function addNoCorsDomain(url) {
// Since the caller of this always ensures a protocol is present
// we can safely use the URL constructor to get the origin
// and add it to the no-cors domains list
const uri = new URL(url);
const domain = uri.origin;
if (requestConfig.noCorsDomains.indexOf(domain) === -1) {
requestConfig.noCorsDomains.push(domain);
}
}
/**
* Is the origin of the passed url in the no-cors domains list?
* @param url
* @returns
*/
function isNoCorsDomain(url) {
let result = false;
if (requestConfig.noCorsDomains.length) {
// is the current url in the no-cors domains?
const origin = new URL(url).origin.toLowerCase();
result = requestConfig.noCorsDomains.some((domain) => {
return origin.includes(domain);
});
}
return result;
}
/**
* Is the origin of the passed url in the no-cors domains list
* and do we need to send a no-cors request?
*
* @param url
* @returns
*/
function isNoCorsRequestRequired(url) {
let result = false;
// is the current origin in the no-cors domains?
if (isNoCorsDomain(url)) {
const origin = new URL(url).origin.toLowerCase();
// check if we have sent a no-cors request to this domain in the last hour
const lastRequest = requestConfig.crossOriginNoCorsDomains[origin] || 0;
if (Date.now() - 60 * 60000 > lastRequest) {
result = true;
}
}
return result;
}
/* Copyright (c) 2017-2018 Environmental Systems Research Institute, Inc.
* Apache-2.0 */
/**
* Method used internally to surface messages to developers.
*/
function warn(message) {
if (console && console.warn) {
console.warn.apply(console, [message]);
}
}
function getFetch() {
return Promise.resolve({
fetch: globalThis.fetch,
Headers: globalThis.Headers,
Response: globalThis.Response,
Request: globalThis.Request
});
}
/**
* Is the given URL the same origin as the current window?
* Used to determine if we need to do any additional cross-origin
* handling for the request.
* @param url
* @param win - optional window object to use for origin comparison
* (useful for testing)
* @returns
*/
function isSameOrigin(url, win) {
var _a;
/* istanbul ignore next */
if ((!win && !window) || !url) {
return false;
}
else {
win = win || window;
const origin = (_a = win.location) === null || _a === void 0 ? void 0 : _a.origin;
return url.startsWith(origin);
}
}
/* Copyright (c) 2017-2018 Environmental Systems Research Institute, Inc.
* Apache-2.0 */
const NODEJS_DEFAULT_REFERER_HEADER = `@esri/arcgis-rest-js`;
/**
* Sets the default options that will be passed in **all requests across all `@esri/arcgis-rest-js` modules**.
*
* ```js
* import { setDefaultRequestOptions } from "@esri/arcgis-rest-request";
*
* setDefaultRequestOptions({
* authentication: ArcGISIdentityManager // all requests will use this session by default
* })
* ```
*
* You should **never** set a default `authentication` when you are in a server side environment where you may be handling requests for many different authenticated users.
*
* @param options The default options to pass with every request. Existing default will be overwritten.
* @param hideWarnings Silence warnings about setting default `authentication` in shared environments.
*/
function setDefaultRequestOptions(options, hideWarnings) {
if (options.authentication && !hideWarnings) {
warn("You should not set `authentication` as a default in a shared environment such as a web server which will process multiple users requests. You can call `setDefaultRequestOptions` with `true` as a second argument to disable this warning.");
}
globalThis.DEFAULT_ARCGIS_REQUEST_OPTIONS = options;
}
function getDefaultRequestOptions() {
return (globalThis.DEFAULT_ARCGIS_REQUEST_OPTIONS || {
httpMethod: "POST",
params: {
f: "json"
}
});
}
/**
* This error is thrown when a request encounters an invalid token error. Requests that use {@linkcode ArcGISIdentityManager} or
* {@linkcode ApplicationCredentialsManager} in the `authentication` option the authentication manager will automatically try to generate
* a fresh token using either {@linkcode ArcGISIdentityManager.refreshCredentials} or
* {@linkcode ApplicationCredentialsManager.refreshCredentials}. If the request with the new token fails you will receive an `ArcGISAuthError`
* if refreshing the token fails you will receive an instance of {@linkcode ArcGISTokenRequestError}.
*
* ```js
* request(someUrl, {
* authentication: identityManager,
* // some additional options...
* }).catch(e => {
* if(e.name === "ArcGISAuthError") {
* console.log("Request with a new token failed you might want to have the user authorize again.")
* }
*
* if(e.name === "ArcGISTokenRequestError") {
* console.log("There was an error refreshing the token you might want to have the user authorize again.")
* }
* })
* ```
*/
class ArcGISAuthError extends ArcGISRequestError {
/**
* Create a new `ArcGISAuthError` object.
*
* @param message - The error message from the API
* @param code - The error code from the API
* @param response - The original response from the API that caused the error
* @param url - The original url of the request
* @param options - The original options of the request
*/
constructor(message = "AUTHENTICATION_ERROR", code = "AUTHENTICATION_ERROR_CODE", response, url, options) {
super(message, code, response, url, options);
this.name = "ArcGISAuthError";
this.message =
code === "AUTHENTICATION_ERROR_CODE" ? message : `${code}: ${message}`;
// restore prototype chain, see https://stackoverflow.com/questions/41102060/typescript-extending-error-class
// we don't need to check for Object.setPrototypeOf as in the answers because we are ES2017 now.
// Also see https://github.com/Microsoft/TypeScript-wiki/blob/main/Breaking-Changes.md#extending-built-ins-like-error-array-and-map-may-no-longer-work
// and https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Error#custom_error_types
const actualProto = new.target.prototype;
Object.setPrototypeOf(this, actualProto);
}
retry(getSession, retryLimit = 1) {
let tries = 0;
const retryRequest = (resolve, reject) => {
tries = tries + 1;
getSession(this.url, this.options)
.then((session) => {
const newOptions = Object.assign(Object.assign({}, this.options), { authentication: session });
return internalRequest(this.url, newOptions);
})
.then((response) => {
resolve(response);
})
.catch((e) => {
if (e.name === "ArcGISAuthError" && tries < retryLimit) {
retryRequest(resolve, reject);
}
else if (e.name === this.name &&
e.message === this.message &&
tries >= retryLimit) {
reject(this);
}
else {
reject(e);
}
});
};
return new Promise((resolve, reject) => {
retryRequest(resolve, reject);
});
}
}
/**
* Checks for errors in a JSON response from the ArcGIS REST API. If there are no errors, it will return the `data` passed in. If there is an error, it will throw an `ArcGISRequestError` or `ArcGISAuthError`.
*
* @param data The response JSON to check for errors.
* @param url The url of the original request
* @param params The parameters of the original request
* @param options The options of the original request
* @returns The data that was passed in the `data` parameter
*/
function checkForErrors(response, url, params, options, originalAuthError) {
// this is an error message from billing.arcgis.com backend
if (response.code >= 400) {
const { message, code } = response;
throw new ArcGISRequestError(message, code, response, url, options);
}
// error from ArcGIS Online or an ArcGIS Portal or server instance.
if (response.error) {
const { message, code, messageCode } = response.error;
const errorCode = messageCode || code || "UNKNOWN_ERROR_CODE";
if (code === 498 || code === 499) {
if (originalAuthError) {
throw originalAuthError;
}
else {
throw new ArcGISAuthError(message, errorCode, response, url, options);
}
}
throw new ArcGISRequestError(message, errorCode, response, url, options);
}
// error from a status check
if (response.status === "failed" || response.status === "failure") {
let message;
let code = "UNKNOWN_ERROR_CODE";
try {
message = JSON.parse(response.statusMessage).message;
code = JSON.parse(response.statusMessage).code;
}
catch (e) {
message = response.statusMessage || response.message;
}
throw new ArcGISRequestError(message, code, response, url, options);
}
return response;
}
/**
* This is the internal implementation of `request` without the automatic retry behavior to prevent
* infinite loops when a server continues to return invalid token errors.
*
* @param url - The URL of the ArcGIS REST API endpoint.
* @param requestOptions - Options for the request, including parameters relevant to the endpoint.
* @returns A Promise that will resolve with the data from the response.
* @internal
*/
function internalRequest(url, requestOptions) {
const defaults = getDefaultRequestOptions();
const options = Object.assign(Object.assign(Object.assign({ httpMethod: "POST" }, defaults), requestOptions), {
params: Object.assign(Object.assign({}, defaults.params), requestOptions.params),
headers: Object.assign(Object.assign({}, defaults.headers), requestOptions.headers)
});
const { httpMethod, rawResponse } = options;
const params = Object.assign({ f: "json" }, options.params);
let originalAuthError = null;
const fetchOptions = {
method: httpMethod,
signal: options.signal,
/* ensures behavior mimics XMLHttpRequest.
needed to support sending IWA cookies */
credentials: options.credentials || "same-origin"
};
// Is this a no-cors domain? if so we need to set credentials to include
if (isNoCorsDomain(url)) {
fetchOptions.credentials = "include";
}
// the /oauth2/platformSelf route will add X-Esri-Auth-Client-Id header
// and that request needs to send cookies cross domain
// so we need to set the credentials to "include"
if (options.headers &&
options.headers["X-Esri-Auth-Client-Id"] &&
url.indexOf("/oauth2/platformSelf") > -1) {
fetchOptions.credentials = "include";
}
let authentication;
// Check to see if this is a raw token as a string and create a IAuthenticationManager like object for it.
// Otherwise this just assumes that options.authentication is an IAuthenticationManager.
if (typeof options.authentication === "string") {
const rawToken = options.authentication;
authentication = {
portal: "https://www.arcgis.com/sharing/rest",
getToken: () => {
return Promise.resolve(rawToken);
}
};
/* istanbul ignore else - we don't need to test NOT warning people */
if (!options.authentication.startsWith("AAPK") &&
!options.authentication.startsWith("AATK") && // doesn't look like an API Key
!options.suppressWarnings && // user doesn't want to suppress warnings for this request
!globalThis.ARCGIS_REST_JS_SUPPRESS_TOKEN_WARNING // we haven't shown the user this warning yet
) {
warn(`Using an oAuth 2.0 access token directly in the token option is discouraged. Consider using ArcGISIdentityManager or Application session. See https://esriurl.com/arcgis-rest-js-direct-token-warning for more information.`);
globalThis.ARCGIS_REST_JS_SUPPRESS_TOKEN_WARNING = true;
}
}
else {
authentication = options.authentication;
}
// for errors in GET requests we want the URL passed to the error to be the URL before
// query params are applied.
const originalUrl = url;
// default to false, for nodejs
let sameOrigin = false;
// if we are in a browser, check if the url is same origin
/* istanbul ignore else */
if (typeof window !== "undefined") {
sameOrigin = isSameOrigin(url);
}
const requiresNoCors = !sameOrigin && isNoCorsRequestRequired(url);
// the /oauth2/platformSelf route will add X-Esri-Auth-Client-Id header
// and that request needs to send cookies cross domain
// so we need to set the credentials to "include"
if (options.headers &&
options.headers["X-Esri-Auth-Client-Id"] &&
url.indexOf("/oauth2/platformSelf") > -1) {
fetchOptions.credentials = "include";
}
// Simple first promise that we may turn into the no-cors request
let firstPromise = Promise.resolve();
if (requiresNoCors) {
// ensure we send cookies on the request after
fetchOptions.credentials = "include";
firstPromise = sendNoCorsRequest(url);
}
return firstPromise
.then(() => authentication
? authentication.getToken(url).catch((err) => {
/**
* append original request url and requestOptions
* to the error thrown by getToken()
* to assist with retrying
*/
err.url = url;
err.options = options;
/**
* if an attempt is made to talk to an unfederated server
* first try the request anonymously. if a 'token required'
* error is thrown, throw the UNFEDERATED error then.
*/
originalAuthError = err;
return Promise.resolve("");
})
: Promise.resolve(""))
.then((token) => {
if (token.length) {
params.token = token;
}
if (authentication && authentication.getDomainCredentials) {
fetchOptions.credentials = authentication.getDomainCredentials(url);
}
// Custom headers to add to request. IRequestOptions.headers with merge over requestHeaders.
const requestHeaders = {};
if (fetchOptions.method === "GET") {
// Prevents token from being passed in query params when hideToken option is used.
/* istanbul ignore if - window is always defined in a browser. Test case is covered by Jasmine in node test */
if (params.token &&
options.hideToken &&
// Sharing API does not support preflight check required by modern browsers https://developer.mozilla.org/en-US/docs/Glossary/Preflight_request
typeof window === "undefined") {
requestHeaders["X-Esri-Authorization"] = `Bearer ${params.token}`;
delete params.token;
}
// encode the parameters into the query string
const queryParams = encodeQueryString(params);
// dont append a '?' unless parameters are actually present
const urlWithQueryString = queryParams === "" ? url : url + "?" + encodeQueryString(params);
if (
// This would exceed the maximum length for URLs by 2000 as default or as specified by the consumer and requires POST
(options.maxUrlLength &&
urlWithQueryString.length > options.maxUrlLength) ||
(!options.maxUrlLength && urlWithQueryString.length > 2000) ||
// Or if the customer requires the token to be hidden and it has not already been hidden in the header (for browsers)
(params.token && options.hideToken)) {
// the consumer specified a maximum length for URLs
// and this would exceed it, so use post instead
fetchOptions.method = "POST";
// If the token was already added as a Auth header, add the token back to body with other params instead of header
if (token.length && options.hideToken) {
params.token = token;
// Remove existing header that was added before url query length was checked
delete requestHeaders["X-Esri-Authorization"];
}
}
else {
// just use GET
url = urlWithQueryString;
}
}
/* updateResources currently requires FormData even when the input parameters dont warrant it.
https://developers.arcgis.com/rest/users-groups-and-items/update-resources.htm
see https://github.com/Esri/arcgis-rest-js/pull/500 for more info. */
const forceFormData = new RegExp("/items/.+/updateResources").test(url);
if (fetchOptions.method === "POST") {
fetchOptions.body = encodeFormData(params, forceFormData);
}
// Mixin headers from request options
fetchOptions.headers = Object.assign(Object.assign({}, requestHeaders), options.headers);
// This should have the same conditional for Node JS as ArcGISIdentityManager.refreshWithUsernameAndPassword()
// to ensure that generated tokens have the same referer when used in Node with a username and password.
/* istanbul ignore next - karma reports coverage on browser tests only */
if ((typeof window === "undefined" ||
(window && typeof window.document === "undefined")) &&
!fetchOptions.headers.referer) {
fetchOptions.headers.referer = NODEJS_DEFAULT_REFERER_HEADER;
}
/* istanbul ignore else blob responses are difficult to make cross platform we will just have to trust the isomorphic fetch will do its job */
if (!requiresFormData(params) && !forceFormData) {
fetchOptions.headers["Content-Type"] =
"application/x-www-form-urlencoded";
}
/**
* Check for a global fetch first and use it if available. This allows us to use the default
* configuration of fetch-mock in tests.
*/
/* istanbul ignore next coverage is based on browser code and we don't test for the absence of global fetch so we can skip the else here. */
return globalThis.fetch
? globalThis.fetch(url, fetchOptions)
: getFetch().then(({ fetch }) => {
return fetch(url, fetchOptions);
});
})
.then((response) => {
// the request got back an error status code (4xx, 5xx)
if (!response.ok) {
// we need to determine if the server returned a JSON body with more details.
// this is the format used by newer services such as the Places and Style service.
return response
.json()
.then((jsonError) => {
// The body can be parsed as JSON
const { status, statusText } = response;
const { message, details } = jsonError.error;
const formattedMessage = `${message}. ${details ? details.join(" ") : ""}`.trim();
throw new ArcGISRequestError(formattedMessage, `HTTP ${status} ${statusText}`, jsonError, url, options);
})
.catch((e) => {
// if we already were about to format this as an ArcGISRequestError throw that error
if (e.name === "ArcGISRequestError") {
throw e;
}
// server responded w/ an actual error (404, 500, etc) but we could not parse it as JSON
const { status, statusText } = response;
throw new ArcGISRequestError(statusText, `HTTP ${status}`, response, url, options);
});
}
if (rawResponse) {
return response;
}
switch (params.f) {
case "json":
return response.json();
case "geojson":
return response.json();
case "html":
return response.text();
case "text":
return response.text();
/* istanbul ignore next blob responses are difficult to make cross platform we will just have to trust that isomorphic fetch will do its job */
default:
return response.blob();
}
})
.then((data) => {
// Check for an error in the JSON body of a successful response.
// Most ArcGIS Server services will return a successful status code but include an error in the response body.
if ((params.f === "json" || params.f === "geojson") && !rawResponse) {
const response = checkForErrors(data, originalUrl, params, options, originalAuthError);
// If this was a portal/self call, and we got authorizedNoCorsDomains back
// register them
if (data && /\/sharing\/rest\/(accounts|portals)\/self/i.test(url)) {
// if we have a list of no-cors domains, register them
if (Array.isArray(data.authorizedCrossOriginNoCorsDomains)) {
registerNoCorsDomains(data.authorizedCrossOriginNoCorsDomains);
}
}
if (originalAuthError) {
/* If the request was made to an unfederated service that
didn't require authentication, add the base url and a dummy token
to the list of trusted servers to avoid another federation check
in the event of a repeat request */
const truncatedUrl = url
.toLowerCase()
.split(/\/rest(\/admin)?\/services\//)[0];
options.authentication.federatedServers[truncatedUrl] = {
token: [],
// default to 24 hours
expires: new Date(Date.now() + 86400 * 1000)
};
originalAuthError = null;
}
return response;
}
else {
return data;
}
});
}
/**
* Generic method for making HTTP requests to ArcGIS REST API endpoints.
*
* ```js
* import { request } from '@esri/arcgis-rest-request';
*
* request('https://www.arcgis.com/sharing/rest')
* .then(response) // response.currentVersion === 5.2
*
* request('https://www.arcgis.com/sharing/rest', {
* httpMethod: "GET"
* })
*
* request('https://www.arcgis.com/sharing/rest/search', {
* params: { q: 'parks' }
* })
* .then(response) // response.total => 78379
* ```
*
* @param url - The URL of the ArcGIS REST API endpoint.
* @param requestOptions - Options for the request, including parameters relevant to the endpoint.
* @returns A Promise that will resolve with the data from the response.
*/
function request(url, requestOptions = { params: { f: "json" } }) {
const { request } = requestOptions, internalOptions = __rest(requestOptions, ["request"]);
// if the user passed in a custom request function, use that instead of the default
return request
? request(url, internalOptions)
: internalRequest(url, internalOptions).catch((e) => {
if (e instanceof ArcGISAuthError &&
requestOptions.authentication &&
typeof requestOptions.authentication !== "string" &&
requestOptions.authentication.canRefresh &&
requestOptions.authentication.refreshCredentials) {
return e.retry(() => {
return requestOptions.authentication.refreshCredentials();
}, 1);
}
else {
return Promise.reject(e);
}
});
}
/* Copyright (c) 2017-2018 Environmental Systems Research Institute, Inc.
* Apache-2.0 */
/**
* Helper for methods with lots of first order request options to pass through as request parameters.
*/
function appendCustomParams(customOptions, keys, baseOptions) {
// NOTE: this must be kept in sync with the keys in IRequestOptions
const requestOptionsKeys = [
"params",
"httpMethod",
"rawResponse",
"authentication",
"hideToken",
"portal",
"credentials",
"maxUrlLength",
"headers",
"signal",
"suppressWarnings",
"request"
];
const options = Object.assign(Object.assign({ params: {} }, baseOptions), customOptions);
// merge all keys in customOptions into options.params
options.params = keys.reduce((value, key) => {
if (customOptions[key] ||
typeof customOptions[key] === "boolean" ||
(typeof customOptions[key] === "number" &&
customOptions[key] === 0)) {
value[key] = customOptions[key];
}
return value;
}, options.params);
// now remove all properties in options that don't exist in IRequestOptions
return requestOptionsKeys.reduce((value, key) => {
if (options[key]) {
value[key] = options[key];
}
return value;
}, {});
}
/* Copyright (c) 2022 Environmental Systems Research Institute, Inc.
* Apache-2.0 */
/**
* There are 5 potential error codes that might be thrown by {@linkcode ArcGISTokenRequestError}. 2 of these codes are used by both
* {@linkcode ArcGISIdentityManager} or {@linkcode ApplicationCredentialsManager}:
*
* * `TOKEN_REFRESH_FAILED` when a request for an new access token fails.
* * `UNKNOWN_ERROR_CODE` the error is unknown. More information may be available in {@linkcode ArcGISTokenRequestError.response}
*
* The 3 remaining error codes will only be thrown when using {@linkcode ArcGISIdentityManager}:
*
* * `GENERATE_TOKEN_FOR_SERVER_FAILED` when a request for a token for a specific federated server fails.
* * `REFRESH_TOKEN_EXCHANGE_FAILED` when a request for a new refresh token fails.
* * `NOT_FEDERATED` when the requested server isn't federated with the portal specified in {@linkcode ArcGISIdentityManager.portal}.
*/
var ArcGISTokenRequestErrorCodes;
(function (ArcGISTokenRequestErrorCodes) {
ArcGISTokenRequestErrorCodes["TOKEN_REFRESH_FAILED"] = "TOKEN_REFRESH_FAILED";
ArcGISTokenRequestErrorCodes["GENERATE_TOKEN_FOR_SERVER_FAILED"] = "GENERATE_TOKEN_FOR_SERVER_FAILED";
ArcGISTokenRequestErrorCodes["REFRESH_TOKEN_EXCHANGE_FAILED"] = "REFRESH_TOKEN_EXCHANGE_FAILED";
ArcGISTokenRequestErrorCodes["NOT_FEDERATED"] = "NOT_FEDERATED";
ArcGISTokenRequestErrorCodes["UNKNOWN_ERROR_CODE"] = "UNKNOWN_ERROR_CODE";
})(ArcGISTokenRequestErrorCodes || (ArcGISTokenRequestErrorCodes = {}));
/**
* This error is thrown when {@linkcode ArcGISIdentityManager} or {@linkcode ApplicationCredentialsManager} fails to refresh a token or generate a new token
* for a request. Generally in this scenario the credentials are invalid for the request and the you should recreate the {@linkcode ApplicationCredentialsManager}
* or prompt the user to authenticate again with {@linkcode ArcGISIdentityManager}. See {@linkcode ArcGISTokenRequestErrorCodes} for a more detailed description of
* the possible error codes.
*
* ```js
* request(someUrl, {
* authentication: someAuthenticationManager
* }).catch(e => {
* if(e.name === "ArcGISTokenRequestError") {
* // ArcGIS REST JS could not generate an appropriate token for this request
* // All credentials are likely invalid and the authentication process should be restarted
* }
* })
* ```
*/
class ArcGISTokenRequestError extends Error {
/**
* Create a new `ArcGISTokenRequestError` object.
*
* @param message - The error message from the API
* @param code - The error code from the API
* @param response - The original response from the API that caused the error
* @param url - The original url of the request
* @param options - The original options and parameters of the request
*/
constructor(message = "UNKNOWN_ERROR", code = ArcGISTokenRequestErrorCodes.UNKNOWN_ERROR_CODE, response, url, options) {
// 'Error' breaks prototype chain here
super(message);
// restore prototype chain, see https://stackoverflow.com/questions/41102060/typescript-extending-error-class
// we don't need to check for Object.setPrototypeOf as in the answers because we are ES2017 now.
// Also see https://github.com/Microsoft/TypeScript-wiki/blob/main/Breaking-Changes.md#extending-built-ins-like-error-array-and-map-may-no-longer-work
// and https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Error#custom_error_types
const actualProto = new.target.prototype;
Object.setPrototypeOf(this, actualProto);
this.name = "ArcGISTokenRequestError";
this.message = `${code}: ${message}`;
this.originalMessage = message;
this.code = code;
this.response = response;
this.url = url;
this.options = options;
}
}
/* Copyright (c) 2022 Environmental Systems Research Institute, Inc.
* Apache-2.0 */
/**
* This error code will be thrown by the following methods when the user cancels or denies an authorization request on the OAuth 2.0
* authorization screen.
*
* * {@linkcode ArcGISIdentityManager.beginOAuth2} when the `popup` option is `true`
* * {@linkcode ArcGISIdentityManager.completeOAuth2} when the `popup` option is `false`
*
* ```js
* import { ArcGISIdentityManager } from "@esri/arcgis-rest-request";
*
* ArcGISIdentityManager.beginOAuth2({
* clientId: "***"
* redirectUri: "***",
* popup: true
* }).then(authenticationManager => {
* console.log("OAuth 2.0 Successful");
* }).catch(e => {
* if(e.name === "ArcGISAccessDeniedError") {
* console.log("The user did not authorize your app.")
* } else {
* console.log("Something else went wrong. Error:", e);
* }
* })
* ```
*/
class ArcGISAccessDeniedError extends Error {
/**
* Create a new `ArcGISAccessDeniedError` object.
*/
constructor() {
const message = "The user has denied your authorization request.";
super(message);
// restore prototype chain, see https://stackoverflow.com/questions/41102060/typescript-extending-error-class
// we don't need to check for Object.setPrototypeOf as in the answers because we are ES2017 now.
// Also see https://github.com/Microsoft/TypeScript-wiki/blob/main/Breaking-Changes.md#extending-built-ins-like-error-array-and-map-may-no-longer-work
// and https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Error#custom_error_types
const actualProto = new.target.prototype;
Object.setPrototypeOf(this, actualProto);
this.name = "ArcGISAccessDeniedError";
}
}
/* Copyright (c) 2017 Environmental Systems Research Institute, Inc.
* Apache-2.0 */
/**
* This represents a generic error from a {@linkcode Job}. There will be details about the error in the {@linkcode ArcGISJobError.jobInfo}.
*
* ```js
* job.getAllResults().catch(e => {
* if(e.name === "ArcGISJobError") {
* console.log("Something went wrong with the job", e);
* console.log("Full job info", e.jobInfo);
* }
* })
* ```
*/
class ArcGISJobError extends Error {
/**
* Create a new `ArcGISJobError` object.
*
* @param message - The error message from the API
* @param jobInfo - The info of the job that is in an error state
*/
constructor(message = "Unknown error", jobInfo) {
// 'Error' breaks prototype chain here
super(message);
// restore prototype chain, see https://stackoverflow.com/questions/41102060/typescript-extending-error-class
// we don't need to check for Object.setPrototypeOf as in the answers because we are ES2017 now.
// Also see https://github.com/Microsoft/TypeScript-wiki/blob/main/Breaking-Changes.md#extending-built-ins-like-error-array-and-map-may-no-longer-work
// and https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Error#custom_error_types
const actualProto = new.target.prototype;
Object.setPrototypeOf(this, actualProto);
this.name = "ArcGISJobError";
this.message = `${jobInfo.status}: ${message}`;
this.status = jobInfo.status;
this.id = jobInfo.id;
this.jobInfo = jobInfo;
}
}
/* Copyright (c) 2018 Environmental Systems Research Institute, Inc.
* Apache-2.0 */
/**
* Helper method to ensure that user supplied urls don't include whitespace or a trailing slash.
*/
function cleanUrl(url) {
// Guard so we don't try to trim something that's not a string
if (typeof url !== "string") {
return url;
}
// trim leading and trailing spaces, but not spaces inside the url
url = url.trim();
// remove the trailing slash to the url if one was included
if (url[url.length - 1] === "/") {
url = url.slice(0, -1);
}
return url;
}
/* Copyright (c) 2017-2020 Environmental Systems Research Institute, Inc.
* Apache-2.0 */
function decodeParam(param) {
const [key, value] = param.split("=");
return { key: decodeURIComponent(key), value: decodeURIComponent(value) };
}
/**
* Decodes the passed query string as an object.
*
* @param query A string to be decoded.
* @returns A decoded query param object.
*/
function decodeQueryString(query) {
if (!query || query.length <= 0) {
return {};
}
return query
.replace(/^#/, "")
.replace(/^\?/, "")
.split("&")
.reduce((acc, entry) => {
const { key, value } = decodeParam(entry);
acc[key] = value;
return acc;
}, {});
}
/* Copyright (c) 2017 Environmental Systems Research Institute, Inc.
* Apache-2.0 */
/**
* Enum describing the different errors that might be thrown by a request.
*
* ```ts
* import { request, ErrorTypes } from '@esri/arcgis-rest-request';
*
* request("...").catch((e) => {
* switch(e.name) {
* case ErrorType.ArcGISRequestError:
* // handle a general error from the API
* break;
*
* case ErrorType.ArcGISAuthError:
* // handle an authentication error
* break;
*
* case ErrorType.ArcGISAccessDeniedE