Skip to content

Commit 5dac8ac

Browse files
committed
Add redirect support to fetch-proxy
Fixes #38 Closes #69
1 parent 5fc1f3a commit 5dac8ac

File tree

3 files changed

+89
-48
lines changed

3 files changed

+89
-48
lines changed

packages/fetch-proxy/CHANGELOG.md

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,10 @@
22

33
This is the changelog for [`fetch-proxy`](https://github.com/mjackson/remix-the-web/tree/main/packages/fetch-proxy). It follows [semantic versioning](https://semver.org/).
44

5+
## HEAD
6+
7+
- Forward all additional options to the proxied request object
8+
59
## v0.3.0 (2025-06-10)
610

711
- Add `/src` to npm package, so "go to definition" goes to the actual source

packages/fetch-proxy/src/lib/fetch-proxy.test.ts

Lines changed: 68 additions & 31 deletions
Original file line numberDiff line numberDiff line change
@@ -3,70 +3,73 @@ import { describe, it } from 'node:test';
33

44
import { type FetchProxyOptions, createFetchProxy } from './fetch-proxy.ts';
55

6-
async function runProxy(
7-
request: Request,
6+
async function testProxy(
7+
input: URL | RequestInfo,
8+
init: RequestInit | undefined,
89
target: string | URL,
910
options?: FetchProxyOptions,
1011
): Promise<{ request: Request; response: Response }> {
11-
let outgoingRequest: Request;
12+
let request: Request;
1213
let proxy = createFetchProxy(target, {
1314
...options,
14-
async fetch(input, init) {
15-
outgoingRequest = new Request(input, init);
16-
return options?.fetch?.(input, init) ?? new Response();
15+
fetch(input, init) {
16+
request = new Request(input, init);
17+
return options?.fetch?.(input, init) ?? Promise.resolve(new Response());
1718
},
1819
});
1920

20-
let proxyResponse = await proxy(request);
21+
let response = await proxy(input, init);
2122

22-
assert.ok(outgoingRequest!);
23+
assert.ok(request!);
2324

24-
return {
25-
request: outgoingRequest,
26-
response: proxyResponse,
27-
};
25+
return { request, response };
2826
}
2927

3028
describe('fetch proxy', () => {
3129
it('appends the request URL pathname + search to the target URL', async () => {
32-
let { request: request1 } = await runProxy(
33-
new Request('http://shopify.com'),
30+
let { request: request1 } = await testProxy(
31+
'http://shopify.com',
32+
undefined,
3433
'https://remix.run:3000/rsc',
3534
);
3635

3736
assert.equal(request1.url, 'https://remix.run:3000/rsc');
3837

39-
let { request: request2 } = await runProxy(
40-
new Request('http://shopify.com/?q=remix'),
38+
let { request: request2 } = await testProxy(
39+
'http://shopify.com/?q=remix',
40+
undefined,
4141
'https://remix.run:3000/rsc',
4242
);
4343

4444
assert.equal(request2.url, 'https://remix.run:3000/rsc?q=remix');
4545

46-
let { request: request3 } = await runProxy(
47-
new Request('http://shopify.com/search?q=remix'),
46+
let { request: request3 } = await testProxy(
47+
'http://shopify.com/search?q=remix',
48+
undefined,
4849
'https://remix.run:3000/',
4950
);
5051

5152
assert.equal(request3.url, 'https://remix.run:3000/search?q=remix');
5253

53-
let { request: request4 } = await runProxy(
54-
new Request('http://shopify.com/search?q=remix'),
54+
let { request: request4 } = await testProxy(
55+
'http://shopify.com/search?q=remix',
56+
undefined,
5557
'https://remix.run:3000/rsc',
5658
);
5759

5860
assert.equal(request4.url, 'https://remix.run:3000/rsc/search?q=remix');
5961
});
6062

6163
it('forwards request method, headers, and body', async () => {
62-
let { request } = await runProxy(
63-
new Request('http://shopify.com/search?q=remix', {
64+
let { request } = await testProxy(
65+
'http://shopify.com/search?q=remix',
66+
{
6467
method: 'POST',
6568
headers: {
6669
'Content-Type': 'text/plain',
6770
},
6871
body: 'hello',
69-
}),
72+
},
7073
'https://remix.run:3000/rsc',
7174
);
7275

@@ -75,9 +78,24 @@ describe('fetch proxy', () => {
7578
assert.equal(await request.text(), 'hello');
7679
});
7780

81+
it('forwards an empty request body', async () => {
82+
let { request } = await testProxy(
83+
'http://shopify.com/search?q=remix',
84+
{
85+
method: 'POST',
86+
},
87+
'https://remix.run:3000/rsc',
88+
);
89+
90+
assert.equal(request.method, 'POST');
91+
assert.equal(request.headers.get('Content-Type'), null);
92+
assert.equal(await request.text(), '');
93+
});
94+
7895
it('does not append X-Forwarded-Proto and X-Forwarded-Host headers by default', async () => {
79-
let { request } = await runProxy(
80-
new Request('http://shopify.com:8080/search?q=remix'),
96+
let { request } = await testProxy(
97+
'http://shopify.com:8080/search?q=remix',
98+
undefined,
8199
'https://remix.run:3000/rsc',
82100
);
83101

@@ -86,8 +104,9 @@ describe('fetch proxy', () => {
86104
});
87105

88106
it('appends X-Forwarded-Proto and X-Forwarded-Host headers when desired', async () => {
89-
let { request } = await runProxy(
90-
new Request('http://shopify.com:8080/search?q=remix'),
107+
let { request } = await testProxy(
108+
'http://shopify.com:8080/search?q=remix',
109+
undefined,
91110
'https://remix.run:3000/rsc',
92111
{
93112
xForwardedHeaders: true,
@@ -98,9 +117,26 @@ describe('fetch proxy', () => {
98117
assert.equal(request.headers.get('X-Forwarded-Host'), 'shopify.com:8080');
99118
});
100119

120+
it('forwards additional request init options', async () => {
121+
let { request } = await testProxy(
122+
'http://shopify.com/search?q=remix',
123+
{
124+
cache: 'no-cache',
125+
credentials: 'include',
126+
redirect: 'manual',
127+
},
128+
'https://remix.run:3000/rsc',
129+
);
130+
131+
assert.equal(request.cache, 'no-cache');
132+
assert.equal(request.credentials, 'include');
133+
assert.equal(request.redirect, 'manual');
134+
});
135+
101136
it('rewrites cookie domain and path', async () => {
102-
let { response } = await runProxy(
103-
new Request('http://shopify.com/search?q=remix'),
137+
let { response } = await testProxy(
138+
'http://shopify.com/search?q=remix',
139+
undefined,
104140
'https://remix.run:3000/rsc',
105141
{
106142
async fetch() {
@@ -122,8 +158,9 @@ describe('fetch proxy', () => {
122158
});
123159

124160
it('does not rewrite cookie domain and path when opting-out', async () => {
125-
let { response } = await runProxy(
126-
new Request('http://shopify.com/?q=remix'),
161+
let { response } = await testProxy(
162+
'http://shopify.com/?q=remix',
163+
undefined,
127164
'https://remix.run:3000/rsc',
128165
{
129166
rewriteCookieDomain: false,

packages/fetch-proxy/src/lib/fetch-proxy.ts

Lines changed: 17 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -48,27 +48,27 @@ export function createFetchProxy(target: string | URL, options?: FetchProxyOptio
4848
}
4949

5050
return async (input: URL | RequestInfo, init?: RequestInit) => {
51-
let incomingRequest = new Request(input, init);
52-
let incomingUrl = new URL(incomingRequest.url);
51+
let request = new Request(input, init);
52+
let url = new URL(request.url);
5353

54-
let proxyUrl = new URL(incomingUrl.search, targetUrl);
55-
if (incomingUrl.pathname !== '/') {
54+
let proxyUrl = new URL(url.search, targetUrl);
55+
if (url.pathname !== '/') {
5656
proxyUrl.pathname =
57-
proxyUrl.pathname === '/' ? incomingUrl.pathname : proxyUrl.pathname + incomingUrl.pathname;
57+
proxyUrl.pathname === '/' ? url.pathname : proxyUrl.pathname + url.pathname;
5858
}
5959

60-
let proxyHeaders = new Headers(incomingRequest.headers);
60+
let proxyHeaders = new Headers(request.headers);
6161
if (xForwardedHeaders) {
62-
proxyHeaders.append('X-Forwarded-Proto', incomingUrl.protocol.replace(/:$/, ''));
63-
proxyHeaders.append('X-Forwarded-Host', incomingUrl.host);
62+
proxyHeaders.append('X-Forwarded-Proto', url.protocol.replace(/:$/, ''));
63+
proxyHeaders.append('X-Forwarded-Host', url.host);
6464
}
6565

6666
let proxyInit: RequestInit = {
67-
method: incomingRequest.method,
67+
...init,
6868
headers: proxyHeaders,
6969
};
70-
if (incomingRequest.method !== 'GET' && incomingRequest.method !== 'HEAD') {
71-
proxyInit.body = incomingRequest.body;
70+
if (request.method !== 'GET' && request.method !== 'HEAD') {
71+
proxyInit.body = request.body;
7272

7373
// init.duplex = 'half' must be set when body is a ReadableStream, and Node follows the spec.
7474
// However, this property is not defined in the TypeScript types for RequestInit, so we have
@@ -77,8 +77,8 @@ export function createFetchProxy(target: string | URL, options?: FetchProxyOptio
7777
(proxyInit as { duplex: 'half' }).duplex = 'half';
7878
}
7979

80-
let targetResponse = await localFetch(proxyUrl, proxyInit);
81-
let responseHeaders = new Headers(targetResponse.headers);
80+
let response = await localFetch(proxyUrl, proxyInit);
81+
let responseHeaders = new Headers(response.headers);
8282

8383
if (responseHeaders.has('Set-Cookie')) {
8484
let setCookie = responseHeaders.getSetCookie();
@@ -89,7 +89,7 @@ export function createFetchProxy(target: string | URL, options?: FetchProxyOptio
8989
let header = new SetCookie(cookie);
9090

9191
if (rewriteCookieDomain && header.domain) {
92-
header.domain = incomingUrl.host;
92+
header.domain = url.host;
9393
}
9494

9595
if (rewriteCookiePath && header.path) {
@@ -104,9 +104,9 @@ export function createFetchProxy(target: string | URL, options?: FetchProxyOptio
104104
}
105105
}
106106

107-
return new Response(targetResponse.body, {
108-
status: targetResponse.status,
109-
statusText: targetResponse.statusText,
107+
return new Response(response.body, {
108+
status: response.status,
109+
statusText: response.statusText,
110110
headers: responseHeaders,
111111
});
112112
};

0 commit comments

Comments
 (0)