Skip to content

Commit 59c1680

Browse files
committed
fix: support for redirects with 200 status. Fix crashing dev server when have socket error. Follow redirects from proxyRes
1 parent a87fd59 commit 59c1680

File tree

10 files changed

+149
-21
lines changed

10 files changed

+149
-21
lines changed

src/utils/proxy.ts

+23-16
Original file line numberDiff line numberDiff line change
@@ -122,10 +122,14 @@ const formatEdgeFunctionError = (errorBuffer, acceptsHtml) => {
122122
})
123123
}
124124

125-
function isInternal(url?: string): boolean {
125+
function isInternalFunctions(url?: string): boolean {
126126
return url?.startsWith('/.netlify/') ?? false
127127
}
128128

129+
function isInternal(url?: string): boolean {
130+
return url?.startsWith('/') ?? false
131+
}
132+
129133
function isFunction(functionsPort: boolean | number | undefined, url: string) {
130134
return functionsPort && url.match(DEFAULT_FUNCTION_URL_EXPRESSION)
131135
}
@@ -202,7 +206,7 @@ const handleAddonUrl = function ({ addonUrl, req, res }) {
202206

203207
// @ts-expect-error TS(7006) FIXME: Parameter 'match' implicitly has an 'any' type.
204208
const isRedirect = function (match) {
205-
return match.status && match.status >= 300 && match.status <= 400
209+
return match.status && match.status >= 300 && match.status < 400
206210
}
207211

208212
// @ts-expect-error TS(7006) FIXME: Parameter 'publicFolder' implicitly has an 'any' t... Remove this comment to see the full error message
@@ -417,8 +421,8 @@ const serveRedirect = async function ({
417421
const ct = req.headers['content-type'] ? contentType.parse(req).type : ''
418422
if (
419423
req.method === 'POST' &&
420-
!isInternal(req.url) &&
421-
!isInternal(destURL) &&
424+
!isInternalFunctions(req.url) &&
425+
!isInternalFunctions(destURL) &&
422426
(ct.endsWith('/x-www-form-urlencoded') || ct === 'multipart/form-data')
423427
) {
424428
return proxy.web(req, res, { target: options.functionsServer })
@@ -428,14 +432,13 @@ const serveRedirect = async function ({
428432
const matchingFunction =
429433
functionsRegistry &&
430434
(await functionsRegistry.getFunctionForURLPath(destURL, req.method, () => Boolean(destStaticFile)))
431-
let statusValue
432435
if (
433436
match.force ||
434437
(!staticFile && ((!options.framework && destStaticFile) || isInternal(destURL) || matchingFunction))
435438
) {
439+
// 3xx redirects parsed above, here are 2xx meaning just override the url of proxying page and use the status
440+
// which comes from that url
436441
req.url = destStaticFile ? destStaticFile + dest.search : destURL
437-
const { status } = match
438-
statusValue = status
439442
console.log(`${NETLIFYDEVLOG} Rewrote URL to`, req.url)
440443
}
441444

@@ -452,15 +455,17 @@ const serveRedirect = async function ({
452455

453456
return proxy.web(req, res, { headers: functionHeaders, target: options.functionsServer })
454457
}
458+
455459
if (isImageRequest(req)) {
456460
return imageProxy(req, res)
457461
}
462+
458463
const addonUrl = getAddonUrl(options.addonsUrls, req)
459464
if (addonUrl) {
460465
return handleAddonUrl({ req, res, addonUrl })
461466
}
462467

463-
return proxy.web(req, res, { ...options, status: statusValue })
468+
return proxy.web(req, res, options)
464469
}
465470

466471
return proxy.web(req, res, options)
@@ -519,11 +524,14 @@ const initializeProxy = async function ({
519524
}
520525
})
521526

522-
proxy.on('error', (_, req, res) => {
523-
// @ts-expect-error TS(2339) FIXME: Property 'writeHead' does not exist on type 'Socke... Remove this comment to see the full error message
524-
res.writeHead(500, {
525-
'Content-Type': 'text/plain',
526-
})
527+
proxy.on('error', (err, req, res) => {
528+
console.error('Got error from proxy', err)
529+
530+
if (res instanceof http.ServerResponse) {
531+
res.writeHead(500, {
532+
'Content-Type': 'text/plain',
533+
})
534+
}
527535

528536
const message = isEdgeFunctionsRequest(req)
529537
? 'There was an error with an Edge Function. Please check the terminal for more details.'
@@ -596,8 +604,7 @@ const initializeProxy = async function ({
596604
}
597605
}
598606

599-
// @ts-expect-error TS(2339) FIXME: Property 'proxyOptions' does not exist on type 'In... Remove this comment to see the full error message
600-
if (req.proxyOptions.staticFile && isRedirect({ status: proxyRes.statusCode }) && proxyRes.headers.location) {
607+
if (isRedirect({ status: proxyRes.statusCode }) && proxyRes.headers.location) {
601608
req.url = proxyRes.headers.location
602609
return serveRedirect({
603610
// We don't want to match functions at this point because any redirects
@@ -838,7 +845,7 @@ const onRequest = async (
838845
hasFormSubmissionHandler &&
839846
functionsServer &&
840847
req.method === 'POST' &&
841-
!isInternal(req.url) &&
848+
!isInternalFunctions(req.url) &&
842849
(ct.endsWith('/x-www-form-urlencoded') || ct === 'multipart/form-data')
843850
) {
844851
return proxy.web(req, res, { target: functionsServer })

tests/integration/__fixtures__/hugo-site/netlify.toml

+1-1
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,6 @@
22
command = "npm run serve"
33
framework = "#custom"
44
functions = "functions/"
5-
port = 7000
5+
port = 7001
66
publish = "out"
77
targetPort = 1313

tests/integration/__fixtures__/next-app/app/page.js

+1
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@ export default function Home() {
55
return (
66
<main className={styles.main}>
77
<div className={styles.description}>
8+
<p>Local site Dev Server</p>
89
<p>
910
Get started by editing&nbsp;
1011
<code className={styles.code}>app/page.js</code>
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
export default function ExistsPage() {
2+
return <p>Exists page</p>
3+
}

tests/integration/__fixtures__/next-app/netlify.toml

+10
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,16 @@ publish = "public"
44
[dev]
55
targetPort = 6123
66

7+
[[redirects]]
8+
from = "/local/*"
9+
to = "/:splat"
10+
status = 200
11+
12+
[[redirects]]
13+
from = "/test/*"
14+
to = "https://www.netlify.app/:splat"
15+
status = 200
16+
717
[[redirects]]
818
from = "*"
919
to = "https://www.netlify.app/:splat"
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
.netlify
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,29 @@
1+
const http = require('http')
2+
3+
const server = http.createServer((req, res) => {
4+
const pathname = new URL(req.url, 'http://localhost').pathname
5+
6+
console.log(`Got ${pathname}`)
7+
8+
if (pathname === '/') {
9+
res.write('Root page')
10+
res.end()
11+
} else if (pathname === '/test/exists') {
12+
res.writeHead(302, undefined, { location: '/test/exists/' })
13+
res.end()
14+
} else if (pathname === '/test/exists/') {
15+
res.write('Test exists page')
16+
res.end()
17+
} else if (pathname === '/test/not-allowed') {
18+
res.writeHead(405)
19+
res.write('This not allowed')
20+
res.end()
21+
} else {
22+
res.writeHead(404).write('Page is not found')
23+
res.end()
24+
}
25+
})
26+
27+
server.listen(6124, () => {
28+
console.log('Server is Running')
29+
})
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,8 @@
1+
[dev]
2+
targetPort = 6124
3+
command = "npm run dev"
4+
5+
[[redirects]]
6+
from = "/local/*"
7+
to = "/:splat"
8+
status = 200
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,9 @@
1+
{
2+
"name": "site-with-redirect",
3+
"version": "1.0.0",
4+
"private": true,
5+
"scripts": {
6+
"dev": "node index.js"
7+
},
8+
"dependencies": {}
9+
}

tests/integration/commands/dev/redirects.test.ts

+64-4
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,7 @@ import fetch from 'node-fetch'
66
describe('redirects', () => {
77
setupFixtureTests('dev-server-with-functions', { devServer: true }, () => {
88
test<FixtureTestContext>('should send original query params to functions', async ({ devServer }) => {
9-
const response = await fetch(`http://localhost:${devServer.port}/with-params?param2=world&other=1`, {})
9+
const response = await fetch(`http://localhost:${devServer.port}/with-params?param2=world&other=1`)
1010

1111
expect(response.status).toBe(200)
1212

@@ -19,7 +19,7 @@ describe('redirects', () => {
1919
test<FixtureTestContext>('should send original query params to functions when using duplicate parameters', async ({
2020
devServer,
2121
}) => {
22-
const response = await fetch(`http://localhost:${devServer.port}/api/echo?param=hello&param=world`, {})
22+
const response = await fetch(`http://localhost:${devServer.port}/api/echo?param=hello&param=world`)
2323

2424
expect(response.status).toBe(200)
2525

@@ -31,23 +31,83 @@ describe('redirects', () => {
3131

3232
setupFixtureTests('next-app', { devServer: { env: { NETLIFY_DEV_SERVER_CHECK_SSG_ENDPOINTS: 1 } } }, () => {
3333
test<FixtureTestContext>('should prefer local files instead of redirect when not forced', async ({ devServer }) => {
34-
const response = await fetch(`http://localhost:${devServer.port}/test.txt`, {})
34+
const response = await fetch(`http://localhost:${devServer.port}/test.txt`)
3535

3636
expect(response.status).toBe(200)
3737

3838
const result = await response.text()
3939
expect(result.trim()).toEqual('hello world')
40+
expect(devServer?.output).not.toContain('Proxying to https://www.netlify.app')
4041
})
4142

4243
test<FixtureTestContext>('should check for the dynamic page existence before doing redirect', async ({
4344
devServer,
4445
}) => {
45-
const response = await fetch(`http://localhost:${devServer.port}/`, {})
46+
const response = await fetch(`http://localhost:${devServer.port}/`)
4647

4748
expect(response.status).toBe(200)
4849

4950
const result = await response.text()
51+
expect(result.toLowerCase()).toContain('local site dev server')
5052
expect(result.toLowerCase()).not.toContain('netlify')
53+
expect(devServer?.output).not.toContain('Proxying to https://www.netlify.app')
54+
})
55+
56+
test<FixtureTestContext>('nested route redirect check for the page existence', async ({ devServer }) => {
57+
let response = await fetch(`http://localhost:${devServer.port}/test/exists`)
58+
expect(response.status).toBe(200)
59+
60+
let result = await response.text()
61+
expect(result.toLowerCase()).toContain('exists page')
62+
expect(devServer?.output).not.toContain('Proxying to https://www.netlify.app/exists')
63+
64+
response = await fetch(`http://localhost:${devServer.port}/test/about`)
65+
expect(response.status).toBe(200)
66+
67+
result = await response.text()
68+
expect(result.toLowerCase()).toContain('netlify')
69+
70+
expect(devServer?.output).toContain('Proxying to https://www.netlify.app/about')
71+
})
72+
73+
test<FixtureTestContext>('should do local redirect', async ({ devServer }) => {
74+
const response = await fetch(`http://localhost:${devServer.port}/local/test/exists`)
75+
76+
expect(response.status).toBe(200)
77+
78+
const result = await response.text()
79+
expect(response.headers.get('location')).toBeNull()
80+
expect(response.status).toBe(200)
81+
expect(result.toLowerCase()).toContain('exists page')
82+
expect(result.toLowerCase()).not.toContain('netlify')
83+
expect(devServer?.output).not.toContain('Proxying to https://www.netlify.app/test')
84+
})
85+
})
86+
87+
setupFixtureTests('site-with-redirect', { devServer: true }, () => {
88+
test<FixtureTestContext>('should do local redirect', async ({ devServer }) => {
89+
const response = await fetch(`http://localhost:${devServer.port}/local/test/exists`)
90+
91+
expect(response.status).toBe(200)
92+
93+
const result = await response.text()
94+
expect(response.url).toEqual(`http://localhost:${devServer.port}/local/test/exists`)
95+
expect(response.status).toBe(200)
96+
expect(result.toLowerCase()).toContain('exists page')
97+
expect(result.toLowerCase()).not.toContain('netlify')
98+
expect(devServer?.output).not.toContain('Proxying to https://www.netlify.app')
99+
})
100+
101+
test<FixtureTestContext>('should pass proper status code of the redirected page', async ({ devServer }) => {
102+
let response = await fetch(`http://localhost:${devServer.port}/local/test/not-allowed`)
103+
104+
expect(response.status).toBe(405)
105+
const result = await response.text()
106+
expect(result.toLowerCase()).toContain('this not allowed')
107+
108+
response = await fetch(`http://localhost:${devServer.port}/local/test/not-found`)
109+
110+
expect(response.status).toBe(404)
51111
})
52112
})
53113
})

0 commit comments

Comments
 (0)