Skip to content

Commit

Permalink
feat: support redirects
Browse files Browse the repository at this point in the history
  • Loading branch information
eduardoboucas committed Aug 21, 2023
1 parent 0a82c2a commit 2864c59
Show file tree
Hide file tree
Showing 6 changed files with 100 additions and 13 deletions.
55 changes: 42 additions & 13 deletions src/utils/proxy.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -181,7 +181,7 @@ const alternativePathsFor = function (url) {
return paths
}

const serveRedirect = async function ({ env, match, options, proxy, req, res, siteInfo }) {
const serveRedirect = async function ({ env, functionsRegistry, match, options, proxy, req, res, siteInfo }) {
if (!match) return proxy.web(req, res, options)

options = options || req.proxyOptions || {}
Expand Down Expand Up @@ -214,6 +214,7 @@ const serveRedirect = async function ({ env, match, options, proxy, req, res, si
if (isFunction(options.functionsPort, req.url)) {
return proxy.web(req, res, { target: options.functionsServer })
}

const urlForAddons = getAddonUrl(options.addonsUrls, req)
if (urlForAddons) {
return handleAddonUrl({ req, res, addonUrl: urlForAddons })
Expand Down Expand Up @@ -327,22 +328,28 @@ const serveRedirect = async function ({ env, match, options, proxy, req, res, si
return proxy.web(req, res, { target: options.functionsServer })
}

const functionWithCustomRoute = functionsRegistry && (await functionsRegistry.getFunctionForURLPath(destURL))
const destStaticFile = await getStatic(dest.pathname, options.publicFolder)
let statusValue
if (match.force || (!staticFile && ((!options.framework && destStaticFile) || isInternal(destURL)))) {
if (
match.force ||
(!staticFile && ((!options.framework && destStaticFile) || isInternal(destURL) || functionWithCustomRoute))
) {
req.url = destStaticFile ? destStaticFile + dest.search : destURL
const { status } = match
statusValue = status
console.log(`${NETLIFYDEVLOG} Rewrote URL to`, req.url)
}

if (isFunction(options.functionsPort, req.url)) {
if (isFunction(options.functionsPort, req.url) || functionWithCustomRoute) {
const functionHeaders = functionWithCustomRoute ? { [NFFunctionName]: functionWithCustomRoute.name } : {}
const url = reqToURL(req, originalURL)
req.headers['x-netlify-original-pathname'] = url.pathname
req.headers['x-netlify-original-search'] = url.search

return proxy.web(req, res, { target: options.functionsServer })
return proxy.web(req, res, { headers: functionHeaders, target: options.functionsServer })
}

const addonUrl = getAddonUrl(options.addonsUrls, req)
if (addonUrl) {
return handleAddonUrl({ req, res, addonUrl })
Expand Down Expand Up @@ -434,12 +441,22 @@ const initializeProxy = async function ({ configPath, distDir, env, host, port,
}

if (proxyRes.statusCode === 404 || proxyRes.statusCode === 403) {
// If a request for `/path` has failed, we'll a few variations like
// `/path/index.html` to mimic the CDN behavior.
if (req.alternativePaths && req.alternativePaths.length !== 0) {
req.url = req.alternativePaths.shift()
return proxy.web(req, res, req.proxyOptions)
}

// The request has failed but we might still have a matching redirect
// rule (without `force`) that should kick in. This is how we mimic the
// file shadowing behavior from the CDN.
if (req.proxyOptions && req.proxyOptions.match) {
return serveRedirect({
// We don't want to match functions at this point because any redirects
// to functions will have already been processed, so we don't supply a
// functions registry to `serveRedirect`.
functionsRegistry: null,
req,
res,
proxy: handlers,
Expand All @@ -453,7 +470,19 @@ const initializeProxy = async function ({ configPath, distDir, env, host, port,

if (req.proxyOptions.staticFile && isRedirect({ status: proxyRes.statusCode }) && proxyRes.headers.location) {
req.url = proxyRes.headers.location
return serveRedirect({ req, res, proxy: handlers, match: null, options: req.proxyOptions, siteInfo, env })
return serveRedirect({
// We don't want to match functions at this point because any redirects
// to functions will have already been processed, so we don't supply a
// functions registry to `serveRedirect`.
functionsRegistry: null,
req,
res,
proxy: handlers,
match: null,
options: req.proxyOptions,
siteInfo,
env,
})
}

const responseData = []
Expand Down Expand Up @@ -565,20 +594,20 @@ const onRequest = async (
return proxy.web(req, res, { target: edgeFunctionsProxyURL })
}

// Does the request match a function on the fixed URL path?
if (isFunction(settings.functionsPort, req.url)) {
return proxy.web(req, res, { target: functionsServer })
}

if (functionsRegistry) {
const functionMatch = await functionsRegistry.getFunctionForURLPath(req.url)
// Does the request match a function on a custom URL path?
const functionMatch = functionsRegistry ? await functionsRegistry.getFunctionForURLPath(req.url) : null

// Setting an internal header with the function name so that we don't have
// to match the URL again in the functions server.
if (functionMatch) {
// Setting an internal header with the function name so that we don't
// have to match the URL again in the functions server.
const headers = { [NFFunctionName]: functionMatch.name }

if (functionMatch) {
return proxy.web(req, res, { headers, target: functionsServer })
}
return proxy.web(req, res, { headers, target: functionsServer })
}

const addonUrl = getAddonUrl(addonsUrls, req)
Expand All @@ -604,7 +633,7 @@ const onRequest = async (
// We don't want to generate an ETag for 3xx redirects.
req[shouldGenerateETag] = ({ statusCode }) => statusCode < 300 || statusCode >= 400

return serveRedirect({ req, res, proxy, match, options, siteInfo, env })
return serveRedirect({ req, res, proxy, match, options, siteInfo, env, functionsRegistry })
}

// The request will be served by the framework server, which means we want to
Expand Down
Original file line number Diff line number Diff line change
@@ -1,2 +1,27 @@
[build]
publish = "public"

[functions]
directory = "functions"

[[redirects]]
force = true
from = "/v2-to-legacy-with-force"
status = 200
to = "/.netlify/functions/custom-path-literal"

[[redirects]]
from = "/v2-to-legacy-without-force"
status = 200
to = "/.netlify/functions/custom-path-literal"

[[redirects]]
force = true
from = "/v2-to-custom-with-force"
status = 200
to = "/products"

[[redirects]]
from = "/v2-to-custom-without-force"
status = 200
to = "/products"
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
/products from origin
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
/v2-to-custom-without-force from origin
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
/v2-to-legacy-without-force from origin
30 changes: 30 additions & 0 deletions tests/integration/commands/dev/v2-api.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -118,5 +118,35 @@ describe.runIf(gte(version, '18.13.0'))('v2 api', () => {
expect(response.status).toBe(200)
expect(await response.text()).toBe(`With expression path: ${url}`)
})

describe('handles rewrites to a function', () => {
test<FixtureTestContext>('rewrite to legacy URL format with `force: true`', async ({ devServer }) => {
const url = `http://localhost:${devServer.port}/v2-to-legacy-with-force`
const response = await fetch(url)
expect(response.status).toBe(200)
expect(await response.text()).toBe(`With literal path: ${url}`)
})

test<FixtureTestContext>('rewrite to legacy URL format with `force: false`', async ({ devServer }) => {
const url = `http://localhost:${devServer.port}/v2-to-legacy-without-force`
const response = await fetch(url)
expect(response.status).toBe(200)
expect(await response.text()).toBe('/v2-to-legacy-without-force from origin')
})

test<FixtureTestContext>('rewrite to custom URL format with `force: true`', async ({ devServer }) => {
const url = `http://localhost:${devServer.port}/v2-to-custom-with-force`
const response = await fetch(url)
expect(response.status).toBe(200)
expect(await response.text()).toBe(`With literal path: ${url}`)
})

test<FixtureTestContext>('rewrite to custom URL format with `force: false`', async ({ devServer }) => {
const url = `http://localhost:${devServer.port}/v2-to-custom-without-force`
const response = await fetch(url)
expect(response.status).toBe(200)
expect(await response.text()).toBe('/v2-to-custom-without-force from origin')
})
})
})
})

0 comments on commit 2864c59

Please sign in to comment.