Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: return 404 on default URL when function has custom routes #5998

Merged
merged 4 commits into from
Oct 9, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 6 additions & 0 deletions src/lib/functions/netlify-function.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -123,6 +123,12 @@ export default class NetlifyFunction {
}
}

async getBuildData() {
await this.buildQueue

return this.buildData
}

// Compares a new set of source files against a previous one, returning an
// object with two Sets, one with added and the other with deleted files.
getSrcFilesDiff(newSrcFiles) {
Expand Down
27 changes: 27 additions & 0 deletions src/lib/functions/registry.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@ import { getPathInProject } from '../settings.mjs'
import NetlifyFunction from './netlify-function.mjs'
import runtimes from './runtimes/index.mjs'

const DEFAULT_URL_EXPRESSION = /^\/.netlify\/(functions|builders)\/([^/]+).*/
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We should be refactoring this so that the regex is shared with isFunction

cli/src/utils/proxy.mjs

Lines 85 to 91 in 556d545

/**
* @param {boolean|number|undefined} functionsPort
* @param {string} url
*/
function isFunction(functionsPort, url) {
return functionsPort && url.match(/^\/.netlify\/(functions|builders)\/.+/)
}

const ZIP_EXTENSION = '.zip'

export class FunctionsRegistry {
Expand Down Expand Up @@ -123,6 +124,32 @@ export class FunctionsRegistry {
}

async getFunctionForURLPath(urlPath, method) {
const defaultURLMatch = urlPath.match(DEFAULT_URL_EXPRESSION)
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I believe this block should live in NetlifyFunction#matchUrlPath. That'd make the logic a little easier to reason about, as well.

It'd probably result in more regex matchings needed to be done, but I don't think that's a performance concern at the orders of magnitude that functions have in local dev.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

okay, scratch that - would make

if (routes.length !== 0) {
const paths = routes.map((route) => chalk.underline(route.pattern)).join(', ')
warn(
`Function ${chalk.yellow(func.name)} cannot be invoked on ${chalk.underline(
urlPath,
)}, because the function has the following URL paths defined: ${paths}`,
)
return
a lot harder to implement


if (defaultURLMatch) {
const func = this.get(defaultURLMatch[2])

if (!func) {
return
}

const { routes = [] } = await func.getBuildData()

if (routes.length !== 0) {
const paths = routes.map((route) => chalk.underline(route.pattern)).join(', ')

warn(
`Function ${chalk.yellow(func.name)} cannot be invoked on ${chalk.underline(
urlPath,
)}, because the function has the following URL paths defined: ${paths}`,
)

return
}

return { func, route: null }
}

for (const func of this.functions.values()) {
const route = await func.matchURLPath(urlPath, method)

Expand Down
15 changes: 7 additions & 8 deletions src/utils/proxy.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -597,18 +597,17 @@ 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 })
}

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

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.func.name, [NFFunctionRoute]: functionMatch.route.pattern }
/** @type {Record<string, string>} */
const headers = { [NFFunctionName]: functionMatch.func.name }

if (functionMatch.route) {
headers[NFFunctionRoute] = functionMatch.route.pattern
}

return proxy.web(req, res, { headers, target: functionsServer })
}
Expand Down
8 changes: 8 additions & 0 deletions tests/integration/commands/dev/v2-api.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -126,6 +126,14 @@ describe.runIf(gte(version, '18.13.0'))('v2 api', () => {
expect(await response.text()).toBe(`With expression path: {"sku":"netlify"}`)
})

test<FixtureTestContext>('returns 404 when using the default function URL to access a function with custom routes', async ({
devServer,
}) => {
const url = `http://localhost:${devServer.port}/.netlify/functions/custom-path-literal`
const response = await fetch(url)
expect(response.status).toBe(404)
})

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`
Expand Down
Loading