From 6f5f2139756a1abf0a4aa2e8f9738cf2155f1667 Mon Sep 17 00:00:00 2001 From: OTAKE Haruaki Date: Thu, 8 Aug 2024 15:22:57 +0900 Subject: [PATCH 1/6] fix: append `access-control-max-age` header to preflight response --- src/utils/cors.ts | 2 ++ 1 file changed, 2 insertions(+) diff --git a/src/utils/cors.ts b/src/utils/cors.ts index d5e5bdcb..8f800b3f 100644 --- a/src/utils/cors.ts +++ b/src/utils/cors.ts @@ -5,6 +5,7 @@ import { createAllowHeaderHeaders, createCredentialsHeaders, createExposeHeaders, + createMaxAgeHeader, createMethodsHeaders, createOriginHeaders, resolveCorsOptions, @@ -41,6 +42,7 @@ export function appendCorsPreflightHeaders( ...createExposeHeaders(options), ...createMethodsHeaders(options), ...createAllowHeaderHeaders(event, options), + ...createMaxAgeHeader(options), }; for (const [key, value] of Object.entries(headers)) { event.response.headers.append(key, value); From 1c0b60f8455c368426ad48ea6ff4aacdc11f9154 Mon Sep 17 00:00:00 2001 From: OTAKE Haruaki Date: Thu, 8 Aug 2024 15:42:42 +0900 Subject: [PATCH 2/6] fix: fix header types --- src/types/utils/cors.ts | 2 +- src/utils/internal/cors.ts | 2 +- test/unit/cors.test.ts | 4 ++-- 3 files changed, 4 insertions(+), 4 deletions(-) diff --git a/src/types/utils/cors.ts b/src/types/utils/cors.ts index d8aba97a..7d4ab42a 100644 --- a/src/types/utils/cors.ts +++ b/src/types/utils/cors.ts @@ -64,7 +64,7 @@ export type H3AccessControlExposeHeadersHeader = export type H3AccessControlMaxAgeHeader = | { - "access-control-max-age": string; + "access-control-max-age": number; } | H3EmptyHeader; diff --git a/src/utils/internal/cors.ts b/src/utils/internal/cors.ts index 4648e440..1b9d35e6 100644 --- a/src/utils/internal/cors.ts +++ b/src/utils/internal/cors.ts @@ -182,7 +182,7 @@ export function createMaxAgeHeader( const { maxAge } = options; if (maxAge) { - return { "access-control-max-age": maxAge }; + return { "access-control-max-age": Number.parseInt(maxAge, 10) }; } return {}; diff --git a/test/unit/cors.test.ts b/test/unit/cors.test.ts index e56fa727..d495059f 100644 --- a/test/unit/cors.test.ts +++ b/test/unit/cors.test.ts @@ -427,10 +427,10 @@ describe("cors (unit)", () => { }; expect(createMaxAgeHeader(options1)).toEqual({ - "access-control-max-age": "12345", + "access-control-max-age": 12_345, }); expect(createMaxAgeHeader(options2)).toEqual({ - "access-control-max-age": "0", + "access-control-max-age": 0, }); }); }); From 3d8ccdaff99b53713d084eb1ed349a9313018982 Mon Sep 17 00:00:00 2001 From: OTAKE Haruaki Date: Thu, 8 Aug 2024 15:48:06 +0900 Subject: [PATCH 3/6] Revert "fix: fix header types" This reverts commit 1c0b60f8455c368426ad48ea6ff4aacdc11f9154. --- src/types/utils/cors.ts | 2 +- src/utils/internal/cors.ts | 2 +- test/unit/cors.test.ts | 4 ++-- 3 files changed, 4 insertions(+), 4 deletions(-) diff --git a/src/types/utils/cors.ts b/src/types/utils/cors.ts index 7d4ab42a..d8aba97a 100644 --- a/src/types/utils/cors.ts +++ b/src/types/utils/cors.ts @@ -64,7 +64,7 @@ export type H3AccessControlExposeHeadersHeader = export type H3AccessControlMaxAgeHeader = | { - "access-control-max-age": number; + "access-control-max-age": string; } | H3EmptyHeader; diff --git a/src/utils/internal/cors.ts b/src/utils/internal/cors.ts index 1b9d35e6..4648e440 100644 --- a/src/utils/internal/cors.ts +++ b/src/utils/internal/cors.ts @@ -182,7 +182,7 @@ export function createMaxAgeHeader( const { maxAge } = options; if (maxAge) { - return { "access-control-max-age": Number.parseInt(maxAge, 10) }; + return { "access-control-max-age": maxAge }; } return {}; diff --git a/test/unit/cors.test.ts b/test/unit/cors.test.ts index d495059f..e56fa727 100644 --- a/test/unit/cors.test.ts +++ b/test/unit/cors.test.ts @@ -427,10 +427,10 @@ describe("cors (unit)", () => { }; expect(createMaxAgeHeader(options1)).toEqual({ - "access-control-max-age": 12_345, + "access-control-max-age": "12345", }); expect(createMaxAgeHeader(options2)).toEqual({ - "access-control-max-age": 0, + "access-control-max-age": "0", }); }); }); From 540cddd453c58c1577bce97c39aa374c39b86c75 Mon Sep 17 00:00:00 2001 From: OTAKE Haruaki Date: Fri, 30 Aug 2024 17:17:17 +0900 Subject: [PATCH 4/6] test(cors): add appendCorsPreflightHeaders and appendCorsHeaders tests --- test/unit/cors.test.ts | 230 ++++++++++++++++++++++++++++++++++++++++- 1 file changed, 229 insertions(+), 1 deletion(-) diff --git a/test/unit/cors.test.ts b/test/unit/cors.test.ts index e56fa727..b8d8c663 100644 --- a/test/unit/cors.test.ts +++ b/test/unit/cors.test.ts @@ -1,6 +1,12 @@ import type { H3CorsOptions } from "../../src/types"; import { expect, it, describe } from "vitest"; -import { mockEvent, isPreflightRequest, isCorsOriginAllowed } from "../../src"; +import { + mockEvent, + isPreflightRequest, + isCorsOriginAllowed, + appendCorsPreflightHeaders, + appendCorsHeaders, +} from "../../src"; import { resolveCorsOptions, createOriginHeaders, @@ -434,4 +440,226 @@ describe("cors (unit)", () => { }); }); }); + + describe("appendCorsPreflightHeaders", () => { + it("append CORS headers with preflight request", () => { + { + const eventMock = mockEvent("/", { + method: "OPTIONS", + headers: { + origin: "https://example.com", + "access-control-request-method": "GET", + "access-control-request-headers": "CUSTOM-HEADER", + }, + }); + // default options + const options: H3CorsOptions = { + origin: "*", + methods: "*", + allowHeaders: "*", + exposeHeaders: "*", + credentials: false, + maxAge: false, + preflight: { + statusCode: 204, + }, + }; + + appendCorsPreflightHeaders(eventMock, options); + + expect( + eventMock.response.headers.get("access-control-allow-origin"), + ).toEqual("*"); + expect( + eventMock.response.headers.has("access-control-allow-credentials"), + ).toEqual(false); + expect( + eventMock.response.headers.get("access-control-expose-headers"), + ).toEqual("*"); + expect( + eventMock.response.headers.get("access-control-allow-methods"), + ).toEqual("*"); + expect( + eventMock.response.headers.get("access-control-allow-headers"), + ).toEqual("CUSTOM-HEADER"); + expect(eventMock.response.headers.get("vary")).toEqual( + "access-control-request-headers", + ); + expect( + eventMock.response.headers.has("access-control-max-age"), + ).toEqual(false); + } + + { + const eventMock = mockEvent("/", { + method: "OPTIONS", + headers: { + origin: "https://example.com", + "access-control-request-method": "GET", + "access-control-request-headers": "CUSTOM-HEADER", + }, + }); + // exposeHeaders and maxAge + const options: H3CorsOptions = { + origin: "*", + exposeHeaders: ["CUSTOM-HEADER", "Authorization"], + maxAge: "12345", + }; + + appendCorsPreflightHeaders(eventMock, options); + + expect( + eventMock.response.headers.get("access-control-allow-origin"), + ).toEqual("*"); + expect( + eventMock.response.headers.has("access-control-allow-credentials"), + ).toEqual(false); + expect( + eventMock.response.headers.get("access-control-expose-headers"), + ).toEqual("CUSTOM-HEADER,Authorization"); + expect( + eventMock.response.headers.has("access-control-allow-methods"), + ).toEqual(false); + expect( + eventMock.response.headers.get("access-control-allow-headers"), + ).toEqual("CUSTOM-HEADER"); + expect(eventMock.response.headers.get("vary")).toEqual( + "access-control-request-headers", + ); + expect( + eventMock.response.headers.get("access-control-max-age"), + ).toEqual("12345"); + } + + { + const eventMock = mockEvent("/", { + method: "OPTIONS", + headers: { + origin: "https://example.com", + "access-control-request-method": "GET", + }, + }); + // credentials + const options: H3CorsOptions = { + origin: ["https://example.com"], + credentials: true, + }; + + appendCorsPreflightHeaders(eventMock, options); + + expect( + eventMock.response.headers.get("access-control-allow-origin"), + ).toEqual("https://example.com"); + expect(eventMock.response.headers.get("vary")).toEqual("origin"); + expect( + eventMock.response.headers.get("access-control-allow-credentials"), + ).toEqual("true"); + expect( + eventMock.response.headers.has("access-control-expose-headers"), + ).toEqual(false); + expect( + eventMock.response.headers.has("access-control-allow-methods"), + ).toEqual(false); + expect( + eventMock.response.headers.has("access-control-allow-headers"), + ).toEqual(false); + expect( + eventMock.response.headers.has("access-control-max-age"), + ).toEqual(false); + } + }); + }); + + describe("appendCorsHeaders", () => { + it("append CORS headers with preflight request", () => { + { + const eventMock = mockEvent("/", { + method: "OPTIONS", + headers: { + origin: "https://example.com", + "access-control-request-method": "GET", + "access-control-request-headers": "CUSTOM-HEADER", + }, + }); + // default options + const options: H3CorsOptions = { + origin: "*", + methods: "*", + allowHeaders: "*", + exposeHeaders: "*", + credentials: false, + maxAge: false, + preflight: { + statusCode: 204, + }, + }; + + appendCorsHeaders(eventMock, options); + + expect( + eventMock.response.headers.get("access-control-allow-origin"), + ).toEqual("*"); + expect( + eventMock.response.headers.has("access-control-allow-credentials"), + ).toEqual(false); + expect( + eventMock.response.headers.get("access-control-expose-headers"), + ).toEqual("*"); + } + + { + const eventMock = mockEvent("/", { + method: "OPTIONS", + headers: { + origin: "https://example.com", + "access-control-request-method": "GET", + "access-control-request-headers": "CUSTOM-HEADER", + }, + }); + // exposeHeaders and maxAge + const options: H3CorsOptions = { + origin: "*", + exposeHeaders: ["CUSTOM-HEADER", "Authorization"], + maxAge: "12345", + }; + + appendCorsHeaders(eventMock, options); + + expect( + eventMock.response.headers.get("access-control-allow-origin"), + ).toEqual("*"); + expect( + eventMock.response.headers.has("access-control-allow-credentials"), + ).toEqual(false); + expect( + eventMock.response.headers.get("access-control-expose-headers"), + ).toEqual("CUSTOM-HEADER,Authorization"); + } + + { + const eventMock = mockEvent("/", { + method: "OPTIONS", + headers: { + origin: "https://example.com", + "access-control-request-method": "GET", + }, + }); + // credentials + const options: H3CorsOptions = { + origin: ["https://example.com"], + credentials: true, + }; + + appendCorsHeaders(eventMock, options); + + expect( + eventMock.response.headers.get("access-control-allow-origin"), + ).toEqual("https://example.com"); + expect(eventMock.response.headers.get("vary")).toEqual("origin"); + expect( + eventMock.response.headers.get("access-control-allow-credentials"), + ).toEqual("true"); + } + }); + }); }); From 4b399349ed0cc0ffb8f4e2ff81267878750da1a3 Mon Sep 17 00:00:00 2001 From: OTAKE Haruaki Date: Fri, 30 Aug 2024 20:04:23 +0900 Subject: [PATCH 5/6] fix(cors): `Access-Control-Expose-Headers` is not included in response when CORS-preflight request --- src/utils/cors.ts | 1 - test/unit/cors.test.ts | 27 +++++++-------------------- 2 files changed, 7 insertions(+), 21 deletions(-) diff --git a/src/utils/cors.ts b/src/utils/cors.ts index 8f800b3f..9e3e6066 100644 --- a/src/utils/cors.ts +++ b/src/utils/cors.ts @@ -39,7 +39,6 @@ export function appendCorsPreflightHeaders( const headers = { ...createOriginHeaders(event, options), ...createCredentialsHeaders(options), - ...createExposeHeaders(options), ...createMethodsHeaders(options), ...createAllowHeaderHeaders(event, options), ...createMaxAgeHeader(options), diff --git a/test/unit/cors.test.ts b/test/unit/cors.test.ts index b8d8c663..b99c9761 100644 --- a/test/unit/cors.test.ts +++ b/test/unit/cors.test.ts @@ -473,9 +473,6 @@ describe("cors (unit)", () => { expect( eventMock.response.headers.has("access-control-allow-credentials"), ).toEqual(false); - expect( - eventMock.response.headers.get("access-control-expose-headers"), - ).toEqual("*"); expect( eventMock.response.headers.get("access-control-allow-methods"), ).toEqual("*"); @@ -502,7 +499,7 @@ describe("cors (unit)", () => { // exposeHeaders and maxAge const options: H3CorsOptions = { origin: "*", - exposeHeaders: ["CUSTOM-HEADER", "Authorization"], + exposeHeaders: ["EXPOSE-HEADER", "Authorization"], maxAge: "12345", }; @@ -514,9 +511,6 @@ describe("cors (unit)", () => { expect( eventMock.response.headers.has("access-control-allow-credentials"), ).toEqual(false); - expect( - eventMock.response.headers.get("access-control-expose-headers"), - ).toEqual("CUSTOM-HEADER,Authorization"); expect( eventMock.response.headers.has("access-control-allow-methods"), ).toEqual(false); @@ -554,9 +548,6 @@ describe("cors (unit)", () => { expect( eventMock.response.headers.get("access-control-allow-credentials"), ).toEqual("true"); - expect( - eventMock.response.headers.has("access-control-expose-headers"), - ).toEqual(false); expect( eventMock.response.headers.has("access-control-allow-methods"), ).toEqual(false); @@ -574,11 +565,10 @@ describe("cors (unit)", () => { it("append CORS headers with preflight request", () => { { const eventMock = mockEvent("/", { - method: "OPTIONS", + method: "GET", headers: { origin: "https://example.com", - "access-control-request-method": "GET", - "access-control-request-headers": "CUSTOM-HEADER", + "CUSTOM-HEADER": "CUSTOM-HEADER-VALUE", }, }); // default options @@ -609,17 +599,15 @@ describe("cors (unit)", () => { { const eventMock = mockEvent("/", { - method: "OPTIONS", + method: "GET", headers: { origin: "https://example.com", - "access-control-request-method": "GET", - "access-control-request-headers": "CUSTOM-HEADER", }, }); // exposeHeaders and maxAge const options: H3CorsOptions = { origin: "*", - exposeHeaders: ["CUSTOM-HEADER", "Authorization"], + exposeHeaders: ["EXPOSE-HEADER", "Authorization"], maxAge: "12345", }; @@ -633,15 +621,14 @@ describe("cors (unit)", () => { ).toEqual(false); expect( eventMock.response.headers.get("access-control-expose-headers"), - ).toEqual("CUSTOM-HEADER,Authorization"); + ).toEqual("EXPOSE-HEADER,Authorization"); } { const eventMock = mockEvent("/", { - method: "OPTIONS", + method: "GET", headers: { origin: "https://example.com", - "access-control-request-method": "GET", }, }); // credentials From 4ba37324b0fa47897cbbd88f27ab8bf592bda9b2 Mon Sep 17 00:00:00 2001 From: Haruaki OTAKE Date: Fri, 30 Aug 2024 20:35:43 +0900 Subject: [PATCH 6/6] test(cors); fix it --- test/unit/cors.test.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/test/unit/cors.test.ts b/test/unit/cors.test.ts index b99c9761..ebe85e7e 100644 --- a/test/unit/cors.test.ts +++ b/test/unit/cors.test.ts @@ -562,7 +562,7 @@ describe("cors (unit)", () => { }); describe("appendCorsHeaders", () => { - it("append CORS headers with preflight request", () => { + it("append CORS headers with CORS request", () => { { const eventMock = mockEvent("/", { method: "GET",