Skip to content

Commit f828bab

Browse files
committed
feat: integrate no-cors into request fn
1 parent 29db594 commit f828bab

File tree

2 files changed

+183
-21
lines changed

2 files changed

+183
-21
lines changed

packages/arcgis-rest-request/src/request.ts

Lines changed: 72 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -9,11 +9,18 @@ import {
99
IRequestOptions,
1010
InternalRequestOptions
1111
} from "./utils/IRequestOptions.js";
12+
import {
13+
isNoCorsDomain,
14+
isNoCorsRequestRequired,
15+
registerNoCorsDomains,
16+
sendNoCorsRequest
17+
} from "./utils/sendNoCorsRequest.js";
1218
import { IParams } from "./utils/IParams.js";
1319
import { warn } from "./utils/warn.js";
1420
import { IRetryAuthError } from "./utils/retryAuthError.js";
1521
import { getFetch } from "@esri/arcgis-rest-fetch";
1622
import { IAuthenticationManager } from "./index.js";
23+
import { isSameOrigin } from "./utils/isSameOrigin.js";
1724

1825
export const NODEJS_DEFAULT_REFERER_HEADER = `@esri/arcgis-rest-js`;
1926

@@ -250,6 +257,11 @@ export function internalRequest(
250257
credentials: options.credentials || "same-origin"
251258
};
252259

260+
// Is this a no-cors domain? if so we need to set credentials to include
261+
if (isNoCorsDomain(url)) {
262+
fetchOptions.credentials = "include";
263+
}
264+
253265
// the /oauth2/platformSelf route will add X-Esri-Auth-Client-Id header
254266
// and that request needs to send cookies cross domain
255267
// so we need to set the credentials to "include"
@@ -296,26 +308,55 @@ export function internalRequest(
296308
// query params are applied.
297309
const originalUrl = url;
298310

299-
return (
300-
authentication
301-
? authentication.getToken(url).catch((err) => {
302-
/**
303-
* append original request url and requestOptions
304-
* to the error thrown by getToken()
305-
* to assist with retrying
306-
*/
307-
err.url = url;
308-
err.options = options;
309-
/**
310-
* if an attempt is made to talk to an unfederated server
311-
* first try the request anonymously. if a 'token required'
312-
* error is thrown, throw the UNFEDERATED error then.
313-
*/
314-
originalAuthError = err;
315-
return Promise.resolve("");
316-
})
317-
: Promise.resolve("")
318-
)
311+
// default to false, for nodejs
312+
let sameOrigin = false;
313+
// if we are in a browser, check if the url is same origin
314+
/* istanbul ignore else */
315+
if (typeof window !== "undefined") {
316+
sameOrigin = isSameOrigin(url);
317+
}
318+
const requiresNoCors = !sameOrigin && isNoCorsRequestRequired(url);
319+
320+
// the /oauth2/platformSelf route will add X-Esri-Auth-Client-Id header
321+
// and that request needs to send cookies cross domain
322+
// so we need to set the credentials to "include"
323+
if (
324+
options.headers &&
325+
options.headers["X-Esri-Auth-Client-Id"] &&
326+
url.indexOf("/oauth2/platformSelf") > -1
327+
) {
328+
fetchOptions.credentials = "include";
329+
}
330+
331+
// Simple first promise that we may turn into the no-cors request
332+
let firstPromise = Promise.resolve();
333+
if (requiresNoCors) {
334+
// ensure we send cookies on the request after
335+
fetchOptions.credentials = "include";
336+
firstPromise = sendNoCorsRequest(url);
337+
}
338+
339+
return firstPromise
340+
.then(() =>
341+
authentication
342+
? authentication.getToken(url).catch((err) => {
343+
/**
344+
* append original request url and requestOptions
345+
* to the error thrown by getToken()
346+
* to assist with retrying
347+
*/
348+
err.url = url;
349+
err.options = options;
350+
/**
351+
* if an attempt is made to talk to an unfederated server
352+
* first try the request anonymously. if a 'token required'
353+
* error is thrown, throw the UNFEDERATED error then.
354+
*/
355+
originalAuthError = err;
356+
return Promise.resolve("");
357+
})
358+
: Promise.resolve("")
359+
)
319360
.then((token) => {
320361
if (token.length) {
321362
params.token = token;
@@ -350,7 +391,8 @@ export function internalRequest(
350391

351392
if (
352393
// This would exceed the maximum length for URLs by 2000 as default or as specified by the consumer and requires POST
353-
(options.maxUrlLength && urlWithQueryString.length > options.maxUrlLength) ||
394+
(options.maxUrlLength &&
395+
urlWithQueryString.length > options.maxUrlLength) ||
354396
(!options.maxUrlLength && urlWithQueryString.length > 2000) ||
355397
// Or if the customer requires the token to be hidden and it has not already been hidden in the header (for browsers)
356398
(params.token && options.hideToken)
@@ -484,6 +526,15 @@ export function internalRequest(
484526
originalAuthError
485527
);
486528

529+
// If this was a portal/self call, and we got authorizedNoCorsDomains back
530+
// register them
531+
if (data && /\/sharing\/rest\/(accounts|portals)\/self/i.test(url)) {
532+
// if we have a list of no-cors domains, register them
533+
if (Array.isArray(data.authorizedCrossOriginNoCorsDomains)) {
534+
registerNoCorsDomains(data.authorizedCrossOriginNoCorsDomains);
535+
}
536+
}
537+
487538
if (originalAuthError) {
488539
/* If the request was made to an unfederated service that
489540
didn't require authentication, add the base url and a dummy token

packages/arcgis-rest-request/test/request.test.ts

Lines changed: 111 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,7 @@ import {
2424
FIVE_DAYS_FROM_NOW,
2525
isNode
2626
} from "../../../scripts/test-helpers.js";
27+
import { requestConfig } from "../src/requestConfig.js";
2728

2829
describe("request()", () => {
2930
afterEach(() => {
@@ -830,4 +831,114 @@ describe("request()", () => {
830831
fail(e);
831832
});
832833
});
834+
835+
describe("no-cors:", () => {
836+
beforeEach(() => {
837+
// Reset requestConfig before each test
838+
requestConfig.pendingNoCorsRequests = {};
839+
requestConfig.noCorsDomains = [];
840+
requestConfig.crossOriginNoCorsDomains = {};
841+
});
842+
843+
it("should send no-cors request as first promise when needed", (done) => {
844+
requestConfig.noCorsDomains = ["https://example.com"];
845+
const url = "https://example.com/resource?foo=bar";
846+
// actual call
847+
fetchMock.post(url, SharingRestInfo);
848+
// no-cors request
849+
fetchMock.get("https://example.com/resource", { status: 200 });
850+
851+
request(url)
852+
.then(() => {
853+
let calls = fetchMock.calls("https://example.com/resource");
854+
expect(calls.length).toBe(1);
855+
856+
// expect the first call to be a no-cors request
857+
const [firstUrl, firstOptions] = calls[0];
858+
expect(firstUrl).toBe("https://example.com/resource");
859+
860+
expect(firstOptions.mode).toEqual("no-cors");
861+
expect(firstOptions.credentials).toEqual("include");
862+
863+
// expect the second call to be a normal request
864+
calls = fetchMock.calls("https://example.com/resource?foo=bar");
865+
expect(calls.length).toBe(1);
866+
const [secondUrl, secondOptions] = calls[0];
867+
expect(secondUrl).toBe("https://example.com/resource?foo=bar");
868+
expect(secondOptions.method).toEqual("POST");
869+
expect(secondOptions.credentials).toEqual("include");
870+
871+
done();
872+
})
873+
.catch((e) => {
874+
fail(e);
875+
});
876+
});
877+
878+
it("should skip no-cors request and and include credentials if already sent", (done) => {
879+
requestConfig.noCorsDomains = ["https://example.com"];
880+
requestConfig.crossOriginNoCorsDomains["https://example.com"] =
881+
Date.now();
882+
const url = "https://example.com/resource";
883+
fetchMock.once(url, SharingRestInfo);
884+
// fetchMock.postOnce(url, { status: 200 });
885+
request(url)
886+
.then(() => {
887+
const [lastUrl, lastOptions] = fetchMock.lastCall()!;
888+
expect(lastUrl).toBe("https://example.com/resource");
889+
expect(lastOptions.credentials).toEqual("include");
890+
done();
891+
})
892+
.catch((e) => {
893+
fail(e);
894+
});
895+
});
896+
897+
it("should register no-cors domains if present on portal/self response", (done) => {
898+
const url =
899+
"https://ent.portal.com/portal/sharing/rest/portals/self?f=json";
900+
fetchMock.post(url, {
901+
authorizedCrossOriginNoCorsDomains: [
902+
"https://server.portal.com",
903+
"https://ent.portal.com"
904+
]
905+
});
906+
request(url)
907+
.then(() => {
908+
const [lastUrl, lastOptions] = fetchMock.lastCall()!;
909+
expect(lastUrl).toBe(url);
910+
expect(requestConfig.noCorsDomains).toEqual([
911+
"https://server.portal.com",
912+
"https://ent.portal.com"
913+
]);
914+
// it should not initialise the crossOriginNoCorsDomains
915+
expect(requestConfig.crossOriginNoCorsDomains).toEqual({});
916+
917+
done();
918+
})
919+
.catch((e) => {
920+
fail(e);
921+
});
922+
});
923+
it("should work without no-cors domains present on portal/self response", (done) => {
924+
const url =
925+
"https://ent.portal.com/portal/sharing/rest/portals/self?f=json";
926+
fetchMock.post(url, {
927+
other: "props"
928+
});
929+
request(url)
930+
.then(() => {
931+
const [lastUrl, lastOptions] = fetchMock.lastCall()!;
932+
expect(lastUrl).toBe(url);
933+
expect(requestConfig.noCorsDomains).toEqual([]);
934+
// it should not initialise the crossOriginNoCorsDomains
935+
expect(requestConfig.crossOriginNoCorsDomains).toEqual({});
936+
937+
done();
938+
})
939+
.catch((e) => {
940+
fail(e);
941+
});
942+
});
943+
});
833944
});

0 commit comments

Comments
 (0)