Skip to content

Commit f7174d0

Browse files
chore: cleanup cors middleware (#3083)
- Remove some indirections - Make tests self contained - Switch tests to `Deno.test` as the LSP doesn't yet recognise `describe/it`
1 parent d072dc2 commit f7174d0

File tree

2 files changed

+289
-292
lines changed

2 files changed

+289
-292
lines changed

src/middlewares/cors.ts

Lines changed: 75 additions & 84 deletions
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,14 @@
11
import type { FreshContext } from "../context.ts";
22
import type { MiddlewareFn } from "./mod.ts";
33

4-
export type CORSOptions = {
4+
export type CORSOptions<State> = {
55
origin:
66
| string
77
| string[]
8-
| ((requestOrigin: string, ctx: FreshContext) => string | undefined | null);
8+
| ((
9+
requestOrigin: string,
10+
ctx: FreshContext<State>,
11+
) => string | undefined | null);
912
allowMethods?: string[];
1013
allowHeaders?: string[];
1114
maxAge?: number;
@@ -51,36 +54,19 @@ export type CORSOptions = {
5154
* // ];
5255
* ```
5356
*/
54-
export function cors<T>(options?: CORSOptions): MiddlewareFn<T> {
55-
const opts: CORSOptions = {
57+
export function cors<State>(options?: CORSOptions<State>): MiddlewareFn<State> {
58+
const opts: CORSOptions<State> = {
5659
origin: "*",
5760
allowMethods: ["GET", "HEAD", "PUT", "POST", "DELETE", "PATCH"],
5861
allowHeaders: [],
5962
exposeHeaders: [],
6063
...options,
6164
};
6265

63-
const findAllowOrigin = ((optsOrigin: CORSOptions["origin"]) => {
64-
if (typeof optsOrigin === "string") {
65-
if (optsOrigin === "*") {
66-
return (_requestOrigin: string, _ctx: FreshContext) => optsOrigin;
67-
} else {
68-
return (requestOrigin: string, _ctx: FreshContext) =>
69-
optsOrigin === requestOrigin ? requestOrigin : null;
70-
}
71-
} else if (typeof optsOrigin === "function") {
72-
return (requestOrigin: string, ctx: FreshContext) =>
73-
optsOrigin(requestOrigin, ctx);
74-
} else {
75-
return (requestOrigin: string, _ctx: FreshContext) =>
76-
optsOrigin.includes(requestOrigin) ? requestOrigin : null;
77-
}
78-
})(opts.origin);
79-
8066
const addHeaderProperties = (
8167
headers: Headers,
8268
allowOrigin: string | null | undefined,
83-
opts: CORSOptions,
69+
opts: CORSOptions<State>,
8470
) => {
8571
if (allowOrigin) {
8672
headers.set("Access-Control-Allow-Origin", allowOrigin);
@@ -98,77 +84,82 @@ export function cors<T>(options?: CORSOptions): MiddlewareFn<T> {
9884
}
9985
};
10086

101-
const OptionsResponse = (
102-
ctx: FreshContext,
103-
allowOrigin: string | null | undefined,
104-
opts: CORSOptions,
105-
varyValues: Set<string>,
106-
) => {
107-
const headers = new Headers();
87+
const optsOrigin = opts.origin;
10888

109-
addHeaderProperties(
110-
headers,
111-
allowOrigin,
112-
opts,
113-
);
89+
return async (ctx) => {
90+
const requestOrigin = ctx.req.headers.get("origin") || "";
11491

115-
if (opts.maxAge != null) {
116-
headers.set("Access-Control-Max-Age", opts.maxAge.toString());
92+
let allowOrigin: string | null = null;
93+
if (typeof optsOrigin === "string") {
94+
if (optsOrigin === "*") {
95+
allowOrigin = optsOrigin;
96+
} else {
97+
allowOrigin = optsOrigin === requestOrigin ? requestOrigin : null;
98+
}
99+
} else if (typeof optsOrigin === "function") {
100+
allowOrigin = optsOrigin(requestOrigin, ctx) ?? null;
101+
} else {
102+
allowOrigin = optsOrigin.includes(requestOrigin) ? requestOrigin : null;
117103
}
118104

119-
if (opts.allowMethods?.length) {
120-
headers.set(
121-
"Access-Control-Allow-Methods",
122-
opts.allowMethods.join(","),
123-
);
105+
const vary = new Set<string>();
106+
// Add 'Origin' to Vary if a specific origin is allowed, not '*'
107+
if (opts.origin !== "*" && allowOrigin && allowOrigin !== "*") {
108+
vary.add("Origin");
124109
}
125110

126-
let effectiveAllowHeaders = opts.allowHeaders;
127-
if (!effectiveAllowHeaders?.length) {
128-
const reqHeaders = ctx.req.headers.get(
129-
"Access-Control-Request-Headers",
130-
);
131-
if (reqHeaders) {
132-
effectiveAllowHeaders = reqHeaders.split(/\s*,\s*/);
133-
}
134-
}
111+
if (ctx.req.method === "OPTIONS") {
112+
const headers = new Headers();
135113

136-
if (effectiveAllowHeaders?.length) {
137-
headers.set(
138-
"Access-Control-Allow-Headers",
139-
effectiveAllowHeaders.join(","),
114+
addHeaderProperties(
115+
headers,
116+
allowOrigin,
117+
opts,
140118
);
141-
varyValues.add("Access-Control-Request-Headers");
142-
}
143119

144-
if (varyValues.size > 0) {
145-
headers.set("Vary", Array.from(varyValues).join(", "));
146-
} else {
147-
headers.delete("Vary"); // Ensure Vary is not set if no conditions met
148-
}
120+
if (opts.maxAge != null) {
121+
headers.set("Access-Control-Max-Age", opts.maxAge.toString());
122+
}
149123

150-
headers.delete("Content-Length");
151-
headers.delete("Content-Type");
124+
if (opts.allowMethods?.length) {
125+
headers.set(
126+
"Access-Control-Allow-Methods",
127+
opts.allowMethods.join(","),
128+
);
129+
}
152130

153-
return new Response(null, {
154-
status: 204,
155-
statusText: "No Content",
156-
headers,
157-
});
158-
};
131+
let allowHeaders = opts.allowHeaders;
132+
if (!allowHeaders?.length) {
133+
const reqHeaders = ctx.req.headers.get(
134+
"Access-Control-Request-Headers",
135+
);
136+
if (reqHeaders) {
137+
allowHeaders = reqHeaders.split(/\s*,\s*/);
138+
}
139+
}
159140

160-
return async (ctx: FreshContext): Promise<Response> => {
161-
const requestOrigin = ctx.req.headers.get("origin") || "";
162-
const allowOrigin = findAllowOrigin(requestOrigin, ctx);
141+
if (allowHeaders?.length) {
142+
headers.set(
143+
"Access-Control-Allow-Headers",
144+
allowHeaders.join(","),
145+
);
146+
vary.add("Access-Control-Request-Headers");
147+
}
163148

164-
const varyValues = new Set<string>();
165-
// Add 'Origin' to Vary if a specific origin is allowed, not '*'
166-
if (opts.origin !== "*" && allowOrigin && allowOrigin !== "*") {
167-
varyValues.add("Origin");
168-
}
149+
if (vary.size > 0) {
150+
headers.set("Vary", Array.from(vary).join(", "));
151+
} else {
152+
headers.delete("Vary"); // Ensure Vary is not set if no conditions met
153+
}
169154

170-
if (ctx.req.method === "OPTIONS") {
171-
return OptionsResponse(ctx, allowOrigin, opts, varyValues);
155+
headers.delete("Content-Length");
156+
headers.delete("Content-Type");
157+
158+
return new Response(null, {
159+
status: 204,
160+
statusText: "No Content",
161+
headers,
162+
});
172163
}
173164

174165
// For non-OPTIONS requests
@@ -181,13 +172,13 @@ export function cors<T>(options?: CORSOptions): MiddlewareFn<T> {
181172
);
182173

183174
// Merge our calculated varyValues with any existing Vary from downstream response
184-
if (varyValues.size > 0) {
185-
const existingVary = res.headers.get("Vary");
186-
if (existingVary) {
187-
existingVary.split(/\s*,\s*/).forEach((v) => varyValues.add(v));
175+
if (vary.size > 0) {
176+
const existing = res.headers.get("Vary");
177+
if (existing) {
178+
existing.split(/\s*,\s*/).forEach((v) => vary.add(v));
188179
}
189180

190-
res.headers.set("Vary", Array.from(varyValues).join(", "));
181+
res.headers.set("Vary", Array.from(vary).join(", "));
191182
}
192183

193184
return res;

0 commit comments

Comments
 (0)