@just-be/wildcard 0.2.0 → 0.4.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/package.json +1 -1
- package/src/handlers/index.ts +1 -0
- package/src/handlers/path-rules.ts +66 -0
- package/src/handlers/rewrite.ts +2 -42
- package/src/handlers/static.ts +37 -7
- package/src/schemas.ts +20 -2
- package/src/utils.test.ts +80 -0
- package/src/utils.ts +96 -0
package/package.json
CHANGED
package/src/handlers/index.ts
CHANGED
|
@@ -0,0 +1,66 @@
|
|
|
1
|
+
import type { StaticConfig } from "../schemas";
|
|
2
|
+
import { matchPath, proxyRequest } from "../utils";
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* Handles path-level redirects and rewrites for static handlers
|
|
6
|
+
* Returns a Response if a rule matches, or null to continue to static file serving
|
|
7
|
+
*/
|
|
8
|
+
export async function handlePathRules(
|
|
9
|
+
request: Request,
|
|
10
|
+
config: StaticConfig
|
|
11
|
+
): Promise<Response | null> {
|
|
12
|
+
const url = new URL(request.url);
|
|
13
|
+
const pathname = url.pathname;
|
|
14
|
+
|
|
15
|
+
// Check redirects first (in array order)
|
|
16
|
+
if (config.redirects?.length) {
|
|
17
|
+
for (const rule of config.redirects) {
|
|
18
|
+
const match = matchPath(rule.path, pathname);
|
|
19
|
+
if (!match.matched) continue;
|
|
20
|
+
|
|
21
|
+
// Build target URL, appending suffix for wildcard patterns
|
|
22
|
+
let targetUrl = rule.url;
|
|
23
|
+
if (rule.path.endsWith("/*") && match.suffix && match.suffix !== "/") {
|
|
24
|
+
targetUrl = rule.url.replace(/\/$/, "") + match.suffix;
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
return Response.redirect(targetUrl, rule.permanent ? 301 : 302);
|
|
28
|
+
}
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
// Check rewrites (in array order)
|
|
32
|
+
if (config.rewrites?.length) {
|
|
33
|
+
for (const rule of config.rewrites) {
|
|
34
|
+
const match = matchPath(rule.path, pathname);
|
|
35
|
+
if (!match.matched) continue;
|
|
36
|
+
|
|
37
|
+
// Check HTTP method
|
|
38
|
+
if (!(rule.allowedMethods as string[]).includes(request.method)) {
|
|
39
|
+
return new Response("Method not allowed", {
|
|
40
|
+
status: 405,
|
|
41
|
+
headers: {
|
|
42
|
+
Allow: rule.allowedMethods.join(", "),
|
|
43
|
+
},
|
|
44
|
+
});
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
// Build target URL for rewrite
|
|
48
|
+
const targetUrl = new URL(rule.url);
|
|
49
|
+
|
|
50
|
+
// For wildcard patterns, use the suffix as the path
|
|
51
|
+
// For exact patterns, use the original pathname
|
|
52
|
+
if (rule.path.endsWith("/*") && match.suffix) {
|
|
53
|
+
targetUrl.pathname = match.suffix;
|
|
54
|
+
} else {
|
|
55
|
+
targetUrl.pathname = url.pathname;
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
targetUrl.search = url.search;
|
|
59
|
+
|
|
60
|
+
return proxyRequest(request, targetUrl);
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
// No path rule matched, continue to static file serving
|
|
65
|
+
return null;
|
|
66
|
+
}
|
package/src/handlers/rewrite.ts
CHANGED
|
@@ -1,8 +1,6 @@
|
|
|
1
1
|
import type { Handler } from "../types";
|
|
2
2
|
import type { RewriteConfig } from "../schemas";
|
|
3
|
-
import {
|
|
4
|
-
|
|
5
|
-
const FETCH_TIMEOUT_MS = 5_000;
|
|
3
|
+
import { proxyRequest } from "../utils";
|
|
6
4
|
|
|
7
5
|
export const handleRewrite: Handler<RewriteConfig> = async (request, config) => {
|
|
8
6
|
const originalUrl = new URL(request.url);
|
|
@@ -22,43 +20,5 @@ export const handleRewrite: Handler<RewriteConfig> = async (request, config) =>
|
|
|
22
20
|
url.pathname = originalUrl.pathname;
|
|
23
21
|
url.search = originalUrl.search;
|
|
24
22
|
|
|
25
|
-
|
|
26
|
-
const safeHeaders = filterSafeHeaders(request.headers);
|
|
27
|
-
|
|
28
|
-
// Create a new request with the target URL and filtered headers
|
|
29
|
-
const modifiedRequest = new Request(url.toString(), {
|
|
30
|
-
method: request.method,
|
|
31
|
-
headers: safeHeaders,
|
|
32
|
-
body: request.method !== "GET" && request.method !== "HEAD" ? request.body : undefined,
|
|
33
|
-
});
|
|
34
|
-
|
|
35
|
-
// Create abort controller for timeout
|
|
36
|
-
const controller = new AbortController();
|
|
37
|
-
const timeoutId = setTimeout(() => controller.abort(), FETCH_TIMEOUT_MS);
|
|
38
|
-
|
|
39
|
-
try {
|
|
40
|
-
const response = await fetch(modifiedRequest, {
|
|
41
|
-
signal: controller.signal,
|
|
42
|
-
});
|
|
43
|
-
|
|
44
|
-
clearTimeout(timeoutId);
|
|
45
|
-
|
|
46
|
-
// Create a new response with the same body but potentially modified headers
|
|
47
|
-
const newResponse = new Response(response.body, response);
|
|
48
|
-
|
|
49
|
-
// Remove content-encoding header because the response body is already decoded
|
|
50
|
-
// by the fetch API, and if we forward this header, the browser will try to
|
|
51
|
-
// decode it again, causing corruption
|
|
52
|
-
newResponse.headers.delete("content-encoding");
|
|
53
|
-
|
|
54
|
-
return newResponse;
|
|
55
|
-
} catch (error) {
|
|
56
|
-
clearTimeout(timeoutId);
|
|
57
|
-
|
|
58
|
-
if (error instanceof Error && error.name === "AbortError") {
|
|
59
|
-
return new Response("Request timeout", { status: 504 });
|
|
60
|
-
}
|
|
61
|
-
|
|
62
|
-
return new Response("Bad gateway", { status: 502 });
|
|
63
|
-
}
|
|
23
|
+
return proxyRequest(request, url);
|
|
64
24
|
};
|
package/src/handlers/static.ts
CHANGED
|
@@ -1,12 +1,17 @@
|
|
|
1
1
|
import type { Handler, FileLoader, FileObject } from "../types";
|
|
2
2
|
import type { StaticConfig } from "../schemas";
|
|
3
3
|
import { getContentType, sanitizePath } from "../utils";
|
|
4
|
+
import { handlePathRules } from "./path-rules";
|
|
4
5
|
|
|
5
6
|
export const handleStatic: Handler<StaticConfig, { fileLoader: FileLoader }> = async (
|
|
6
7
|
request,
|
|
7
8
|
config,
|
|
8
9
|
{ fileLoader }
|
|
9
10
|
) => {
|
|
11
|
+
// Check path-level redirects and rewrites first
|
|
12
|
+
const pathResponse = await handlePathRules(request, config);
|
|
13
|
+
if (pathResponse) return pathResponse;
|
|
14
|
+
|
|
10
15
|
const url = new URL(request.url);
|
|
11
16
|
const { spa, fallback } = config;
|
|
12
17
|
|
|
@@ -17,30 +22,55 @@ export const handleStatic: Handler<StaticConfig, { fileLoader: FileLoader }> = a
|
|
|
17
22
|
const basePath = subdomain;
|
|
18
23
|
|
|
19
24
|
return spa
|
|
20
|
-
? handleSpaMode(fileLoader, basePath)
|
|
25
|
+
? handleSpaMode(fileLoader, basePath, url.pathname)
|
|
21
26
|
: handleStaticMode(fileLoader, basePath, url.pathname, fallback);
|
|
22
27
|
};
|
|
23
28
|
|
|
24
|
-
async function handleSpaMode(
|
|
25
|
-
|
|
29
|
+
async function handleSpaMode(
|
|
30
|
+
fileLoader: FileLoader,
|
|
31
|
+
basePath: string,
|
|
32
|
+
pathname: string
|
|
33
|
+
): Promise<Response> {
|
|
34
|
+
// Sanitize paths to prevent directory traversal
|
|
26
35
|
const sanitizedBase = sanitizePath(basePath);
|
|
36
|
+
const sanitizedPathname = sanitizePath(pathname);
|
|
37
|
+
|
|
27
38
|
if (!sanitizedBase) {
|
|
28
39
|
return new Response("Invalid path", { status: 400 });
|
|
29
40
|
}
|
|
30
41
|
|
|
31
|
-
|
|
42
|
+
// First, try to serve the requested file if it exists
|
|
43
|
+
// Only check files that likely exist (have extensions or end with /)
|
|
44
|
+
if (pathname !== "/" && (pathname.includes(".") || pathname.endsWith("/"))) {
|
|
45
|
+
const fullPath = `${sanitizedBase}/${sanitizedPathname}`;
|
|
46
|
+
let key = fullPath.replace(/\/+/g, "/"); // Normalize double slashes
|
|
47
|
+
|
|
48
|
+
// Append index.html for directories
|
|
49
|
+
if (key.endsWith("/")) {
|
|
50
|
+
key = `${key}index.html`;
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
const fileObject = await fileLoader.loadFile(key);
|
|
54
|
+
|
|
55
|
+
if (fileObject) {
|
|
56
|
+
return buildFileResponse(fileObject, key);
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
// Fall back to index.html for SPA routing
|
|
61
|
+
const indexKey = sanitizedBase.endsWith("index.html")
|
|
32
62
|
? sanitizedBase
|
|
33
63
|
: sanitizedBase.endsWith("/")
|
|
34
64
|
? `${sanitizedBase}index.html`
|
|
35
65
|
: `${sanitizedBase}/index.html`;
|
|
36
66
|
|
|
37
|
-
const
|
|
67
|
+
const indexFile = await fileLoader.loadFile(indexKey);
|
|
38
68
|
|
|
39
|
-
if (!
|
|
69
|
+
if (!indexFile) {
|
|
40
70
|
return new Response("File not found", { status: 404 });
|
|
41
71
|
}
|
|
42
72
|
|
|
43
|
-
return buildFileResponse(
|
|
73
|
+
return buildFileResponse(indexFile, indexKey);
|
|
44
74
|
}
|
|
45
75
|
|
|
46
76
|
async function handleStaticMode(
|
package/src/schemas.ts
CHANGED
|
@@ -13,12 +13,30 @@ export const subdomain = () =>
|
|
|
13
13
|
message: "Invalid subdomain format. Must be alphanumeric with hyphens, 1-63 characters.",
|
|
14
14
|
});
|
|
15
15
|
|
|
16
|
+
const HttpMethod = z.enum(["GET", "POST", "PUT", "DELETE", "PATCH", "HEAD", "OPTIONS"]);
|
|
17
|
+
|
|
18
|
+
/** Path-level redirect schema */
|
|
19
|
+
export const PathRedirectSchema = z.object({
|
|
20
|
+
path: z.string().min(1).startsWith("/"),
|
|
21
|
+
url: safeUrl(),
|
|
22
|
+
permanent: z.boolean().optional(),
|
|
23
|
+
});
|
|
24
|
+
|
|
25
|
+
/** Path-level rewrite schema */
|
|
26
|
+
export const PathRewriteSchema = z.object({
|
|
27
|
+
path: z.string().min(1).startsWith("/"),
|
|
28
|
+
url: safeUrl(),
|
|
29
|
+
allowedMethods: z.array(HttpMethod).optional().default(["GET", "HEAD", "OPTIONS"]),
|
|
30
|
+
});
|
|
31
|
+
|
|
16
32
|
// Zod schemas for validation
|
|
17
33
|
export const StaticConfigSchema = z
|
|
18
34
|
.object({
|
|
19
35
|
type: z.literal("static"),
|
|
20
36
|
spa: z.boolean().optional(),
|
|
21
37
|
fallback: z.string().optional(), // Fallback file path for non-SPA mode (e.g., "404.html")
|
|
38
|
+
redirects: z.array(PathRedirectSchema).optional(),
|
|
39
|
+
rewrites: z.array(PathRewriteSchema).optional(),
|
|
22
40
|
})
|
|
23
41
|
.refine((data) => (data.spa && data.fallback ? false : true), {
|
|
24
42
|
message: "fallback cannot be used with spa mode (spa: true)",
|
|
@@ -30,8 +48,6 @@ export const RedirectConfigSchema = z.object({
|
|
|
30
48
|
permanent: z.boolean().optional(),
|
|
31
49
|
});
|
|
32
50
|
|
|
33
|
-
const HttpMethod = z.enum(["GET", "POST", "PUT", "DELETE", "PATCH", "HEAD", "OPTIONS"]);
|
|
34
|
-
|
|
35
51
|
export const RewriteConfigSchema = z.object({
|
|
36
52
|
type: z.literal("rewrite"),
|
|
37
53
|
url: safeUrl(),
|
|
@@ -70,6 +86,8 @@ const json = <T extends z.ZodType>(schema: T) =>
|
|
|
70
86
|
*/
|
|
71
87
|
export const RouteConfigCodec = json(RouteConfigSchema);
|
|
72
88
|
|
|
89
|
+
export type PathRedirect = z.infer<typeof PathRedirectSchema>;
|
|
90
|
+
export type PathRewrite = z.infer<typeof PathRewriteSchema>;
|
|
73
91
|
export type StaticConfig = z.infer<typeof StaticConfigSchema>;
|
|
74
92
|
export type RedirectConfig = z.infer<typeof RedirectConfigSchema>;
|
|
75
93
|
export type RewriteConfig = z.infer<typeof RewriteConfigSchema>;
|
package/src/utils.test.ts
CHANGED
|
@@ -5,6 +5,7 @@ import {
|
|
|
5
5
|
sanitizePath,
|
|
6
6
|
filterSafeHeaders,
|
|
7
7
|
isValidSubdomain,
|
|
8
|
+
matchPath,
|
|
8
9
|
SAFE_REQUEST_HEADERS,
|
|
9
10
|
} from "./utils";
|
|
10
11
|
|
|
@@ -341,3 +342,82 @@ describe("isValidSubdomain", () => {
|
|
|
341
342
|
});
|
|
342
343
|
});
|
|
343
344
|
});
|
|
345
|
+
|
|
346
|
+
describe("matchPath", () => {
|
|
347
|
+
describe("exact match patterns", () => {
|
|
348
|
+
it("should match exact paths", () => {
|
|
349
|
+
expect(matchPath("/github", "/github")).toEqual({ matched: true });
|
|
350
|
+
expect(matchPath("/about", "/about")).toEqual({ matched: true });
|
|
351
|
+
expect(matchPath("/foo/bar", "/foo/bar")).toEqual({ matched: true });
|
|
352
|
+
});
|
|
353
|
+
|
|
354
|
+
it("should not match different paths", () => {
|
|
355
|
+
expect(matchPath("/github", "/gitlab")).toEqual({ matched: false });
|
|
356
|
+
expect(matchPath("/about", "/about/team")).toEqual({ matched: false });
|
|
357
|
+
expect(matchPath("/foo", "/foo/bar")).toEqual({ matched: false });
|
|
358
|
+
});
|
|
359
|
+
|
|
360
|
+
it("should handle trailing slash normalization", () => {
|
|
361
|
+
expect(matchPath("/github", "/github/")).toEqual({ matched: true });
|
|
362
|
+
expect(matchPath("/github/", "/github")).toEqual({ matched: true });
|
|
363
|
+
expect(matchPath("/foo/bar/", "/foo/bar")).toEqual({ matched: true });
|
|
364
|
+
});
|
|
365
|
+
|
|
366
|
+
it("should handle root path", () => {
|
|
367
|
+
expect(matchPath("/", "/")).toEqual({ matched: true });
|
|
368
|
+
});
|
|
369
|
+
});
|
|
370
|
+
|
|
371
|
+
describe("wildcard patterns", () => {
|
|
372
|
+
it("should match prefix with wildcard", () => {
|
|
373
|
+
expect(matchPath("/api/*", "/api/users")).toEqual({ matched: true, suffix: "/users" });
|
|
374
|
+
expect(matchPath("/api/*", "/api/users/123")).toEqual({
|
|
375
|
+
matched: true,
|
|
376
|
+
suffix: "/users/123",
|
|
377
|
+
});
|
|
378
|
+
expect(matchPath("/v1/api/*", "/v1/api/test")).toEqual({ matched: true, suffix: "/test" });
|
|
379
|
+
});
|
|
380
|
+
|
|
381
|
+
it("should match just the prefix without trailing content", () => {
|
|
382
|
+
expect(matchPath("/api/*", "/api")).toEqual({ matched: true, suffix: "/" });
|
|
383
|
+
expect(matchPath("/api/*", "/api/")).toEqual({ matched: true, suffix: "/" });
|
|
384
|
+
});
|
|
385
|
+
|
|
386
|
+
it("should not match paths that do not start with prefix", () => {
|
|
387
|
+
expect(matchPath("/api/*", "/apiv2/users")).toEqual({ matched: false });
|
|
388
|
+
expect(matchPath("/api/*", "/other/api/users")).toEqual({ matched: false });
|
|
389
|
+
expect(matchPath("/foo/*", "/foobar")).toEqual({ matched: false });
|
|
390
|
+
});
|
|
391
|
+
|
|
392
|
+
it("should return correct suffix for nested paths", () => {
|
|
393
|
+
expect(matchPath("/old/*", "/old/page")).toEqual({ matched: true, suffix: "/page" });
|
|
394
|
+
expect(matchPath("/old/*", "/old/deep/nested/path")).toEqual({
|
|
395
|
+
matched: true,
|
|
396
|
+
suffix: "/deep/nested/path",
|
|
397
|
+
});
|
|
398
|
+
});
|
|
399
|
+
|
|
400
|
+
it("should handle wildcard at root", () => {
|
|
401
|
+
expect(matchPath("/*", "/anything")).toEqual({ matched: true, suffix: "/anything" });
|
|
402
|
+
expect(matchPath("/*", "/")).toEqual({ matched: true, suffix: "/" });
|
|
403
|
+
});
|
|
404
|
+
});
|
|
405
|
+
|
|
406
|
+
describe("edge cases", () => {
|
|
407
|
+
it("should handle paths with query strings (pathname only)", () => {
|
|
408
|
+
// matchPath only receives pathname, not query strings
|
|
409
|
+
expect(matchPath("/api/*", "/api/users")).toEqual({ matched: true, suffix: "/users" });
|
|
410
|
+
});
|
|
411
|
+
|
|
412
|
+
it("should handle patterns with multiple segments", () => {
|
|
413
|
+
expect(matchPath("/a/b/c/*", "/a/b/c/d")).toEqual({ matched: true, suffix: "/d" });
|
|
414
|
+
expect(matchPath("/a/b/c/*", "/a/b/c")).toEqual({ matched: true, suffix: "/" });
|
|
415
|
+
});
|
|
416
|
+
|
|
417
|
+
it("should distinguish between similar paths", () => {
|
|
418
|
+
expect(matchPath("/api", "/api")).toEqual({ matched: true });
|
|
419
|
+
expect(matchPath("/api", "/api/")).toEqual({ matched: true });
|
|
420
|
+
expect(matchPath("/api", "/api/users")).toEqual({ matched: false });
|
|
421
|
+
});
|
|
422
|
+
});
|
|
423
|
+
});
|
package/src/utils.ts
CHANGED
|
@@ -163,6 +163,45 @@ export function filterSafeHeaders(headers: Headers): Headers {
|
|
|
163
163
|
return filtered;
|
|
164
164
|
}
|
|
165
165
|
|
|
166
|
+
/**
|
|
167
|
+
* Match result from matchPath function
|
|
168
|
+
*/
|
|
169
|
+
export interface PathMatchResult {
|
|
170
|
+
matched: boolean;
|
|
171
|
+
suffix?: string;
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
/**
|
|
175
|
+
* Matches a path pattern against a pathname
|
|
176
|
+
* Supports exact matches and wildcard suffix patterns (e.g., /api/*)
|
|
177
|
+
*
|
|
178
|
+
* @param pattern - The pattern to match (e.g., "/github" or "/api/*")
|
|
179
|
+
* @param pathname - The actual pathname to match against
|
|
180
|
+
* @returns Object with matched boolean and optional suffix for wildcard matches
|
|
181
|
+
*/
|
|
182
|
+
export function matchPath(pattern: string, pathname: string): PathMatchResult {
|
|
183
|
+
// Normalize paths by removing trailing slashes (except for root "/")
|
|
184
|
+
const normPattern =
|
|
185
|
+
pattern === "/" ? "/" : pattern.endsWith("/") ? pattern.slice(0, -1) : pattern;
|
|
186
|
+
const normPathname =
|
|
187
|
+
pathname === "/" ? "/" : pathname.endsWith("/") ? pathname.slice(0, -1) : pathname;
|
|
188
|
+
|
|
189
|
+
// Handle wildcard patterns: /api/*
|
|
190
|
+
if (normPattern.endsWith("/*")) {
|
|
191
|
+
const prefix = normPattern.slice(0, -2);
|
|
192
|
+
|
|
193
|
+
// Match prefix exactly or prefix + "/"
|
|
194
|
+
if (normPathname === prefix || normPathname.startsWith(prefix + "/")) {
|
|
195
|
+
const suffix = normPathname.slice(prefix.length) || "/";
|
|
196
|
+
return { matched: true, suffix };
|
|
197
|
+
}
|
|
198
|
+
return { matched: false };
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
// Exact match
|
|
202
|
+
return { matched: normPathname === normPattern };
|
|
203
|
+
}
|
|
204
|
+
|
|
166
205
|
/**
|
|
167
206
|
* Validates subdomain name format and length
|
|
168
207
|
* @param subdomain - The subdomain to validate
|
|
@@ -196,3 +235,60 @@ export function isValidSubdomain(subdomain: string): boolean {
|
|
|
196
235
|
|
|
197
236
|
return true;
|
|
198
237
|
}
|
|
238
|
+
|
|
239
|
+
/**
|
|
240
|
+
* Timeout for proxy requests in milliseconds
|
|
241
|
+
*/
|
|
242
|
+
const FETCH_TIMEOUT_MS = 5_000;
|
|
243
|
+
|
|
244
|
+
/**
|
|
245
|
+
* Proxies a request to a target URL with timeout and safe header filtering
|
|
246
|
+
*
|
|
247
|
+
* @param request - The original request
|
|
248
|
+
* @param targetUrl - The target URL to proxy to
|
|
249
|
+
* @returns Response from the proxied request
|
|
250
|
+
*/
|
|
251
|
+
export async function proxyRequest(
|
|
252
|
+
request: Request,
|
|
253
|
+
targetUrl: URL
|
|
254
|
+
): Promise<Response> {
|
|
255
|
+
// Filter headers to only include safe ones (prevents header injection)
|
|
256
|
+
const safeHeaders = filterSafeHeaders(request.headers);
|
|
257
|
+
|
|
258
|
+
// Create a new request with the target URL and filtered headers
|
|
259
|
+
const modifiedRequest = new Request(targetUrl.toString(), {
|
|
260
|
+
method: request.method,
|
|
261
|
+
headers: safeHeaders,
|
|
262
|
+
body: request.method !== "GET" && request.method !== "HEAD" ? request.body : undefined,
|
|
263
|
+
});
|
|
264
|
+
|
|
265
|
+
// Create abort controller for timeout
|
|
266
|
+
const controller = new AbortController();
|
|
267
|
+
const timeoutId = setTimeout(() => controller.abort(), FETCH_TIMEOUT_MS);
|
|
268
|
+
|
|
269
|
+
try {
|
|
270
|
+
const response = await fetch(modifiedRequest, {
|
|
271
|
+
signal: controller.signal,
|
|
272
|
+
});
|
|
273
|
+
|
|
274
|
+
clearTimeout(timeoutId);
|
|
275
|
+
|
|
276
|
+
// Create a new response with the same body but potentially modified headers
|
|
277
|
+
const newResponse = new Response(response.body, response);
|
|
278
|
+
|
|
279
|
+
// Remove content-encoding header because the response body is already decoded
|
|
280
|
+
// by the fetch API, and if we forward this header, the browser will try to
|
|
281
|
+
// decode it again, causing corruption
|
|
282
|
+
newResponse.headers.delete("content-encoding");
|
|
283
|
+
|
|
284
|
+
return newResponse;
|
|
285
|
+
} catch (error) {
|
|
286
|
+
clearTimeout(timeoutId);
|
|
287
|
+
|
|
288
|
+
if (error instanceof Error && error.name === "AbortError") {
|
|
289
|
+
return new Response("Request timeout", { status: 504 });
|
|
290
|
+
}
|
|
291
|
+
|
|
292
|
+
return new Response("Bad gateway", { status: 502 });
|
|
293
|
+
}
|
|
294
|
+
}
|