@megabudino/stack-utils 1.1.0 → 1.2.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.
@@ -11,85 +11,92 @@ export function createProxyHandle(config) {
11
11
  const sitemapCache = `public, max-age=${config.sitemapCacheSeconds ?? 3600}`;
12
12
  const proxyAssets = config.proxyAssets ?? false;
13
13
  const additionalOrigins = config.additionalOrigins ?? [];
14
+ const maxRedirects = config.maxInternalRedirects ?? 5;
14
15
  const textReplacements = config.textReplacements ?? {};
16
+ const siteHost = new URL(siteUrl).hostname;
17
+ const originHosts = new Set([
18
+ new URL(originUrl).hostname,
19
+ ...additionalOrigins.map((url) => new URL(url).hostname)
20
+ ]);
15
21
  return async ({ event, resolve }) => {
16
22
  const { pathname, search } = event.url;
17
23
  if (event.route.id) {
18
24
  return resolve(event);
19
25
  }
20
- const upstream = new URL(pathname + search, originUrl);
21
26
  const headers = new Headers(event.request.headers);
22
27
  headers.delete('accept-encoding');
23
- headers.delete('host');
28
+ headers.set('host', siteHost);
24
29
  headers.delete('content-length');
25
- let upstreamResponse;
26
- try {
27
- upstreamResponse = await fetch(upstream, {
28
- method: event.request.method,
29
- headers,
30
- redirect: 'manual',
31
- body: event.request.method === 'GET' || event.request.method === 'HEAD'
32
- ? undefined
33
- : event.request.body
34
- });
35
- }
36
- catch {
37
- return new Response('Bad Gateway', { status: 502 });
38
- }
39
- if (upstreamResponse.status >= 300 && upstreamResponse.status < 400) {
40
- const location = upstreamResponse.headers.get('location');
41
- if (location) {
42
- try {
43
- const nextUrl = new URL(location);
30
+ let currentPath = pathname + search;
31
+ for (let hops = 0;; hops++) {
32
+ const upstream = new URL(currentPath, originUrl);
33
+ let upstreamResponse;
34
+ try {
35
+ upstreamResponse = await fetch(upstream, {
36
+ method: event.request.method,
37
+ headers,
38
+ redirect: 'manual',
39
+ body: event.request.method === 'GET' || event.request.method === 'HEAD'
40
+ ? undefined
41
+ : event.request.body
42
+ });
43
+ }
44
+ catch {
45
+ return new Response('Bad Gateway', { status: 502 });
46
+ }
47
+ if (upstreamResponse.status >= 300 && upstreamResponse.status < 400) {
48
+ const location = upstreamResponse.headers.get('location');
49
+ if (location) {
50
+ const nextUrl = new URL(location, upstream);
51
+ const isOrigin = originHosts.has(nextUrl.hostname);
52
+ const isSite = nextUrl.hostname === siteHost;
53
+ if ((isOrigin || isSite) && hops < maxRedirects) {
54
+ currentPath = nextUrl.pathname + nextUrl.search;
55
+ continue;
56
+ }
44
57
  return new Response(null, {
45
58
  status: upstreamResponse.status,
46
59
  headers: {
47
- location: `${siteUrl}${nextUrl.pathname}${nextUrl.search}`
60
+ location: isOrigin ? `${siteUrl}${nextUrl.pathname}${nextUrl.search}` : location
48
61
  }
49
62
  });
50
63
  }
51
- catch {
52
- return new Response(null, {
53
- status: upstreamResponse.status,
54
- headers: { location }
55
- });
56
- }
57
64
  }
58
- }
59
- const cleanHeaders = new Headers(upstreamResponse.headers);
60
- cleanHeaders.delete('content-encoding');
61
- cleanHeaders.delete('content-length');
62
- cleanHeaders.delete('transfer-encoding');
63
- const contentType = upstreamResponse.headers.get('content-type') ?? '';
64
- if (contentType.includes('text/xml') || contentType.includes('application/xml')) {
65
- let xml = await upstreamResponse.text();
66
- xml = xml.replace(/<\?xml-stylesheet[^?]*\?>\s*/g, '');
67
- if (sitemaps.length > 0 && xml.includes('</sitemapindex>')) {
68
- xml = injectIntoSitemapIndex(xml, siteUrl, sitemaps);
65
+ const cleanHeaders = new Headers(upstreamResponse.headers);
66
+ cleanHeaders.delete('content-encoding');
67
+ cleanHeaders.delete('content-length');
68
+ cleanHeaders.delete('transfer-encoding');
69
+ const contentType = upstreamResponse.headers.get('content-type') ?? '';
70
+ if (contentType.includes('text/xml') || contentType.includes('application/xml')) {
71
+ let xml = await upstreamResponse.text();
72
+ xml = xml.replace(/<\?xml-stylesheet[^?]*\?>\s*/g, '');
73
+ if (sitemaps.length > 0 && xml.includes('</sitemapindex>')) {
74
+ xml = injectIntoSitemapIndex(xml, siteUrl, sitemaps);
75
+ }
76
+ cleanHeaders.set('cache-control', sitemapCache);
77
+ return new Response(xml, {
78
+ status: upstreamResponse.status,
79
+ statusText: upstreamResponse.statusText,
80
+ headers: cleanHeaders
81
+ });
69
82
  }
70
- cleanHeaders.set('cache-control', sitemapCache);
71
- return new Response(xml, {
72
- status: upstreamResponse.status,
73
- statusText: upstreamResponse.statusText,
74
- headers: cleanHeaders
75
- });
76
- }
77
- if (contentType.includes('text/html')) {
78
- const html = await upstreamResponse.text();
79
- let result = rewriteHtml(html, originUrl, siteUrl, proxyAssets, additionalOrigins);
80
- for (const [search, replace] of Object.entries(textReplacements)) {
81
- result = result.replaceAll(search, replace);
83
+ if (contentType.includes('text/html')) {
84
+ const html = await upstreamResponse.text();
85
+ let result = rewriteHtml(html, originUrl, siteUrl, proxyAssets, additionalOrigins);
86
+ for (const [search, replace] of Object.entries(textReplacements)) {
87
+ result = result.replaceAll(search, replace);
88
+ }
89
+ return new Response(result, {
90
+ status: upstreamResponse.status,
91
+ statusText: upstreamResponse.statusText,
92
+ headers: cleanHeaders
93
+ });
82
94
  }
83
- return new Response(result, {
95
+ return new Response(upstreamResponse.body, {
84
96
  status: upstreamResponse.status,
85
97
  statusText: upstreamResponse.statusText,
86
98
  headers: cleanHeaders
87
99
  });
88
100
  }
89
- return new Response(upstreamResponse.body, {
90
- status: upstreamResponse.status,
91
- statusText: upstreamResponse.statusText,
92
- headers: cleanHeaders
93
- });
94
101
  };
95
102
  }
@@ -25,6 +25,12 @@ export interface ProxyConfig {
25
25
  * rewritten the same way as `originUrl` (e.g. legacy/migration domains).
26
26
  */
27
27
  additionalOrigins?: string[];
28
+ /**
29
+ * Maximum number of internal redirects to follow when the origin responds
30
+ * with a location pointing back to the origin or public site host.
31
+ * Defaults to 5.
32
+ */
33
+ maxInternalRedirects?: number;
28
34
  /**
29
35
  * Key-value map of text replacements applied to HTML responses
30
36
  * after all standard URL rewrites. Each key is replaced with its value
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@megabudino/stack-utils",
3
- "version": "1.1.0",
3
+ "version": "1.2.0",
4
4
  "description": "Reusable utilities for a SvelteKit stack, including a reverse proxy and static image pipeline",
5
5
  "type": "module",
6
6
  "exports": {