Skip to content

Commit 2e08e65

Browse files
authored
refactor: Normalize CORS errors (#24)
1 parent 8478d00 commit 2e08e65

File tree

3 files changed

+67
-28
lines changed

3 files changed

+67
-28
lines changed

src/cors.js

+58-17
Original file line numberDiff line numberDiff line change
@@ -87,6 +87,28 @@ function isSimpleRangeHeader(range) {
8787
// Exports
8888
//-----------------------------------------------------------------------------
8989

90+
/**
91+
* Represents an error that occurs when a CORS request is blocked.
92+
*/
93+
export class CorsError extends Error {
94+
95+
/**
96+
* The name of the error.
97+
* @type {string}
98+
*/
99+
name = "CorsError";
100+
101+
/**
102+
* Creates a new instance.
103+
* @param {string} requestUrl The URL of the request.
104+
* @param {string} origin The origin of the client making the request.
105+
* @param {string} message The error message.
106+
*/
107+
constructor(requestUrl, origin, message) {
108+
super(`Access to fetch at '${requestUrl}' from origin '${origin}' has been blocked by CORS policy: ${message}`);
109+
}
110+
}
111+
90112
/**
91113
* Asserts that the response has the correct CORS headers.
92114
* @param {Response} response The response to check.
@@ -98,14 +120,18 @@ export function assertCorsResponse(response, origin) {
98120
const originHeader = response.headers.get(CORS_ALLOW_ORIGIN);
99121

100122
if (!originHeader) {
101-
throw new Error(
102-
`Access to fetch at '${response.url}' from origin '${origin}' has been blocked by CORS policy: Response to preflight request doesn't pass access control check: No 'Access-Control-Allow-Origin' header is present on the requested resource.`,
123+
throw new CorsError(
124+
response.url,
125+
origin,
126+
"Response to preflight request doesn't pass access control check: No 'Access-Control-Allow-Origin' header is present on the requested resource."
103127
);
104128
}
105129

106130
if (originHeader !== "*" && originHeader !== origin) {
107-
throw new Error(
108-
`Access to fetch at '${response.url}' from origin '${origin}' has been blocked by CORS policy: The 'Access-Control-Allow-Origin' header has a value '${originHeader}' that is not equal to the supplied origin.`,
131+
throw new CorsError(
132+
response.url,
133+
origin,
134+
`The 'Access-Control-Allow-Origin' header has a value '${originHeader}' that is not equal to the supplied origin.`,
109135
);
110136
}
111137

@@ -234,29 +260,39 @@ export class CorsPreflightData {
234260

235261
/**
236262
* Validates a method against the preflight data.
237-
* @param {string} method The method to validate.
263+
* @param {Request} request The request with the method to validate.
264+
* @param {string} origin The origin of the request.
238265
* @returns {void}
239266
* @throws {Error} When the method is not allowed.
240267
*/
241-
#validateMethod(method) {
268+
#validateMethod(request, origin) {
269+
270+
const method = request.method.toUpperCase();
271+
242272
if (
243273
!this.allowAllMethods &&
244274
!corsSafeMethods.has(method) &&
245275
!this.allowedMethods.has(method)
246276
) {
247-
throw new Error(
248-
`Request is blocked by CORS policy: Method ${method} is not allowed.`,
277+
throw new CorsError(
278+
request.url,
279+
origin,
280+
`Method ${method} is not allowed.`,
249281
);
250282
}
251283
}
252284

253285
/**
254286
* Validates a set of headers against the preflight data.
255-
* @param {Headers} headers The headers to validate.
287+
* @param {Request} request The request with headers to validate.
288+
* @param {string} origin The origin of the request.
256289
* @returns {void}
257290
* @throws {Error} When the headers are not allowed.
258291
*/
259-
#validateHeaders(headers) {
292+
#validateHeaders(request, origin) {
293+
294+
const { headers } = request;
295+
260296
for (const header of headers.keys()) {
261297
// simple headers are always allowed
262298
if (corsSafeHeaders.has(header)) {
@@ -268,14 +304,18 @@ export class CorsPreflightData {
268304
header === "authorization" &&
269305
!this.allowedHeaders.has(header)
270306
) {
271-
throw new Error(
272-
`Request is blocked by CORS policy: Header ${header} is not allowed.`,
307+
throw new CorsError(
308+
request.url,
309+
origin,
310+
`Header ${header} is not allowed.`,
273311
);
274312
}
275313

276314
if (!this.allowAllHeaders && !this.allowedHeaders.has(header)) {
277-
throw new Error(
278-
`Request is blocked by CORS policy: Header ${header} is not allowed.`,
315+
throw new CorsError(
316+
request.url,
317+
origin,
318+
`Header ${header} is not allowed.`,
279319
);
280320
}
281321
}
@@ -284,11 +324,12 @@ export class CorsPreflightData {
284324
/**
285325
* Validates a request against the preflight data.
286326
* @param {Request} request The request to validate.
327+
* @param {string} origin The origin of the request.
287328
* @returns {void}
288329
* @throws {Error} When the request is not allowed.
289330
*/
290-
validate(request) {
291-
this.#validateMethod(request.method);
292-
this.#validateHeaders(request.headers);
331+
validate(request, origin) {
332+
this.#validateMethod(request, origin);
333+
this.#validateHeaders(request, origin);
293334
}
294335
}

src/fetch-mocker.js

+3-4
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,7 @@ import {
1515
CORS_REQUEST_METHOD,
1616
CORS_REQUEST_HEADERS,
1717
CORS_ORIGIN,
18+
CorsError
1819
} from "./cors.js";
1920

2021
//-----------------------------------------------------------------------------
@@ -205,7 +206,7 @@ export class FetchMocker {
205206
// if it's not a simple request then we'll need a preflight check
206207
if (!isCorsSimpleRequest(request)) {
207208
preflightData = await this.#preflightFetch(request);
208-
preflightData.validate(request);
209+
preflightData.validate(request, this.#baseUrl.origin);
209210
}
210211

211212
// add the origin header to the request
@@ -315,9 +316,7 @@ export class FetchMocker {
315316

316317
// if the preflight response is successful, then we can make the actual request
317318
if (!preflightResponse.ok) {
318-
throw new Error(
319-
`Request to ${preflightRequest.url} from ${this.#baseUrl.origin} is blocked by CORS policy: Response to preflight request doesn't pass access control check: It does not have HTTP ok status.`,
320-
);
319+
throw new CorsError(preflightRequest.url, this.#baseUrl.origin, "Response to preflight request doesn't pass access control check: It does not have HTTP ok status.");
321320
}
322321

323322
assertCorsResponse(preflightResponse, this.#baseUrl.origin);

tests/fetch-mocker.test.js

+6-7
Original file line numberDiff line numberDiff line change
@@ -74,7 +74,7 @@ Partial matches:
7474
❌ Headers do not match. Expected authorization=Bearer ABC but received authorization=Bearer XYZ.`.trim();
7575

7676
const PREFLIGHT_FAILED = `
77-
Request to https://api.example.com/hello from https://api.example.org is blocked by CORS policy: Response to preflight request doesn't pass access control check: It does not have HTTP ok status.
77+
Access to fetch at 'https://api.example.com/hello' from origin 'https://api.example.org' has been blocked by CORS policy: Response to preflight request doesn't pass access control check: It does not have HTTP ok status.
7878
`.trim();
7979

8080
//-----------------------------------------------------------------------------
@@ -455,7 +455,7 @@ describe("FetchMocker", () => {
455455
server.get("/hello", 200);
456456

457457
await assert.rejects(fetchMocker.fetch(url), {
458-
name: "Error",
458+
name: "CorsError",
459459
message: `Access to fetch at '${url.href}' from origin '${origin}' has been blocked by CORS policy: Response to preflight request doesn't pass access control check: No 'Access-Control-Allow-Origin' header is present on the requested resource.`,
460460
});
461461
});
@@ -472,13 +472,12 @@ describe("FetchMocker", () => {
472472
server.get("/hello", {
473473
status: 200,
474474
headers: {
475-
"Access-Control-Allow-Origin":
476-
"https://api.example.com",
475+
"Access-Control-Allow-Origin": "https://api.example.com",
477476
},
478477
});
479478

480479
await assert.rejects(fetchMocker.fetch(url), {
481-
name: "Error",
480+
name: "CorsError",
482481
message: `Access to fetch at '${url.href}' from origin '${origin}' has been blocked by CORS policy: The 'Access-Control-Allow-Origin' header has a value 'https://api.example.com' that is not equal to the supplied origin.`,
483482
});
484483
});
@@ -891,7 +890,7 @@ describe("FetchMocker", () => {
891890
await assert.rejects(
892891
fetchMocker.fetch(url, { headers: { Custom: "Foo" } }),
893892
{
894-
name: "Error",
893+
name: "CorsError",
895894
message: `Access to fetch at '${url.href}' from origin '${origin}' has been blocked by CORS policy: Response to preflight request doesn't pass access control check: No 'Access-Control-Allow-Origin' header is present on the requested resource.`,
896895
},
897896
);
@@ -919,7 +918,7 @@ describe("FetchMocker", () => {
919918
await assert.rejects(
920919
fetchMocker.fetch(url, { headers: { Custom: "Foo" } }),
921920
{
922-
name: "Error",
921+
name: "CorsError",
923922
message: `Access to fetch at '${url.href}' from origin '${origin}' has been blocked by CORS policy: The 'Access-Control-Allow-Origin' header has a value 'https://api.example.com' that is not equal to the supplied origin.`,
924923
},
925924
);

0 commit comments

Comments
 (0)