netlify-cli 16.1.0 → 16.3.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.
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "netlify-cli",
3
3
  "description": "Netlify command line tool",
4
- "version": "16.1.0",
4
+ "version": "16.3.0",
5
5
  "author": "Netlify Inc.",
6
6
  "type": "module",
7
7
  "engines": {
@@ -44,13 +44,13 @@
44
44
  "dependencies": {
45
45
  "@bugsnag/js": "7.20.2",
46
46
  "@fastify/static": "6.10.2",
47
- "@netlify/build": "29.20.4",
48
- "@netlify/build-info": "7.7.3",
49
- "@netlify/config": "20.8.0",
50
- "@netlify/edge-bundler": "8.17.1",
47
+ "@netlify/build": "29.20.12",
48
+ "@netlify/build-info": "7.8.0",
49
+ "@netlify/config": "20.8.1",
50
+ "@netlify/edge-bundler": "8.19.0",
51
51
  "@netlify/local-functions-proxy": "1.1.1",
52
- "@netlify/serverless-functions-api": "1.5.2",
53
- "@netlify/zip-it-and-ship-it": "9.15.1",
52
+ "@netlify/serverless-functions-api": "1.7.3",
53
+ "@netlify/zip-it-and-ship-it": "9.17.3",
54
54
  "@octokit/rest": "19.0.13",
55
55
  "ansi-escapes": "6.2.0",
56
56
  "ansi-styles": "6.2.1",
@@ -106,7 +106,7 @@
106
106
  "isexe": "2.0.0",
107
107
  "jsonwebtoken": "9.0.1",
108
108
  "jwt-decode": "3.1.2",
109
- "lambda-local": "2.1.1",
109
+ "lambda-local": "2.1.2",
110
110
  "listr": "0.14.3",
111
111
  "locate-path": "7.2.0",
112
112
  "lodash": "4.17.21",
@@ -116,7 +116,7 @@
116
116
  "multiparty": "4.2.3",
117
117
  "netlify": "13.1.10",
118
118
  "netlify-headers-parser": "7.1.2",
119
- "netlify-redirect-parser": "14.1.3",
119
+ "netlify-redirect-parser": "14.2.0",
120
120
  "netlify-redirector": "0.4.0",
121
121
  "node-fetch": "2.6.12",
122
122
  "node-version-alias": "3.4.1",
@@ -161,7 +161,7 @@ const dev = async (options, command) => {
161
161
  },
162
162
  })
163
163
 
164
- await startFunctionsServer({
164
+ const functionsRegistry = await startFunctionsServer({
165
165
  api,
166
166
  command,
167
167
  config,
@@ -217,6 +217,7 @@ const dev = async (options, command) => {
217
217
  geolocationMode: options.geo,
218
218
  geoCountry: options.country,
219
219
  accountId,
220
+ functionsRegistry,
220
221
  })
221
222
 
222
223
  if (devConfig.autoLaunch !== false) {
@@ -37,4 +37,4 @@ const lmInfo = async () => {
37
37
  * @returns
38
38
  */
39
39
  export const createLmInfoCommand = (program) =>
40
- program.command('lm:info').description('Show large media requirements information.').action(lmInfo)
40
+ program.command('lm:info', { hidden: true }).description('Show large media requirements information.').action(lmInfo)
@@ -20,7 +20,7 @@ const lmInstall = async ({ force }) => {
20
20
  */
21
21
  export const createLmInstallCommand = (program) =>
22
22
  program
23
- .command('lm:install')
23
+ .command('lm:install', { hidden: true })
24
24
  .alias('lm:init')
25
25
  .description(
26
26
  `Configures your computer to use Netlify Large Media
@@ -97,7 +97,7 @@ const lmSetup = async (options, command) => {
97
97
  */
98
98
  export const createLmSetupCommand = (program) =>
99
99
  program
100
- .command('lm:setup')
100
+ .command('lm:setup', { hidden: true })
101
101
  .description('Configures your site to use Netlify Large Media')
102
102
  .option('-s, --skip-install', 'Skip the credentials helper installation check')
103
103
  .option('-f, --force-install', 'Force the credentials helper installation')
@@ -25,8 +25,10 @@ export const createLmCommand = (program) => {
25
25
  createLmUninstallCommand(program)
26
26
 
27
27
  program
28
- .command('lm')
29
- .description('Handle Netlify Large Media operations\nThe lm command will help you manage large media for a site')
28
+ .command('lm', { hidden: true })
29
+ .description(
30
+ '[Deprecated and will be removed from future versions] Handle Netlify Large Media operations\nThe lm command will help you manage large media for a site',
31
+ )
30
32
  .addExamples(['netlify lm:info', 'netlify lm:install', 'netlify lm:setup'])
31
33
  .action(lm)
32
34
  }
@@ -93,7 +93,7 @@ const serve = async (options, command) => {
93
93
  options,
94
94
  })
95
95
 
96
- await startFunctionsServer({
96
+ const functionsRegistry = await startFunctionsServer({
97
97
  api,
98
98
  command,
99
99
  config,
@@ -132,7 +132,9 @@ const serve = async (options, command) => {
132
132
  addonsUrls,
133
133
  config,
134
134
  configPath: configPathOverride,
135
+ debug: options.debug,
135
136
  env,
137
+ functionsRegistry,
136
138
  geolocationMode: options.geo,
137
139
  geoCountry: options.country,
138
140
  getUpdatedConfig,
@@ -1,5 +1,5 @@
1
1
  import { env } from 'process'
2
2
 
3
- const latestBootstrapURL = 'https://64c264287e9cbb0008621df3--edge.netlify.com/bootstrap/index-combined.ts'
3
+ const latestBootstrapURL = 'https://64f73321fdd56900083fa618--edge.netlify.com/bootstrap/index-combined.ts'
4
4
 
5
5
  export const getBootstrapURL = () => env.NETLIFY_EDGE_BOOTSTRAP || latestBootstrapURL
@@ -5,10 +5,13 @@ export const headers = {
5
5
  DeployID: 'x-nf-deploy-id',
6
6
  FeatureFlags: 'x-nf-feature-flags',
7
7
  ForwardedHost: 'x-forwarded-host',
8
+ ForwardedProtocol: 'x-forwarded-proto',
8
9
  Functions: 'x-nf-edge-functions',
9
10
  InvocationMetadata: 'x-nf-edge-functions-metadata',
10
11
  Geo: 'x-nf-geo',
11
12
  Passthrough: 'x-nf-passthrough',
13
+ PassthroughHost: 'x-nf-passthrough-host',
14
+ PassthroughProtocol: 'x-nf-passthrough-proto',
12
15
  IP: 'x-nf-client-connection-ip',
13
16
  Site: 'X-NF-Site-Info',
14
17
  DebugLogging: 'x-nf-debug-logging',
@@ -78,6 +78,7 @@ export const createAccountInfoHeader = (accountInfo = {}) => {
78
78
  * @param {boolean=} config.offline
79
79
  * @param {*} config.passthroughPort
80
80
  * @param {*} config.projectDir
81
+ * @param {*} config.settings
81
82
  * @param {*} config.siteInfo
82
83
  * @param {*} config.state
83
84
  * @returns
@@ -96,6 +97,7 @@ export const initializeProxy = async ({
96
97
  offline,
97
98
  passthroughPort,
98
99
  projectDir,
100
+ settings,
99
101
  siteInfo,
100
102
  state,
101
103
  }) => {
@@ -146,7 +148,7 @@ export const initializeProxy = async ({
146
148
  await registry.initialize()
147
149
 
148
150
  const url = new URL(req.url, `http://${LOCAL_HOST}:${mainPort}`)
149
- const { functionNames, invocationMetadata, orphanedDeclarations } = registry.matchURLPath(url.pathname)
151
+ const { functionNames, invocationMetadata, orphanedDeclarations } = registry.matchURLPath(url.pathname, req.method)
150
152
 
151
153
  // If the request matches a config declaration for an Edge Function without
152
154
  // a matching function file, we warn the user.
@@ -167,28 +169,22 @@ export const initializeProxy = async ({
167
169
  }
168
170
 
169
171
  const featureFlags = ['edge_functions_bootstrap_failure_mode']
170
- const forwardedHost = `localhost:${passthroughPort}`
171
172
 
172
173
  req[headersSymbol] = {
173
174
  [headers.FeatureFlags]: getFeatureFlagsHeader(featureFlags),
174
- [headers.ForwardedHost]: forwardedHost,
175
+ [headers.ForwardedProtocol]: settings.https ? 'https:' : 'http:',
175
176
  [headers.Functions]: functionNames.join(','),
176
177
  [headers.InvocationMetadata]: getInvocationMetadataHeader(invocationMetadata),
177
178
  [headers.IP]: LOCAL_HOST,
178
179
  [headers.Passthrough]: 'passthrough',
180
+ [headers.PassthroughHost]: `localhost:${passthroughPort}`,
181
+ [headers.PassthroughProtocol]: 'http:',
179
182
  }
180
183
 
181
184
  if (debug) {
182
185
  req[headersSymbol][headers.DebugLogging] = '1'
183
186
  }
184
187
 
185
- // If we're using a different port for passthrough requests, which is the
186
- // case when the CLI is running on HTTPS, use it on the Host header so
187
- // that the request URL inside the edge function is something accessible.
188
- if (mainPort !== passthroughPort) {
189
- req[headersSymbol].host = forwardedHost
190
- }
191
-
192
188
  return `http://${LOCAL_HOST}:${isolatePort}`
193
189
  }
194
190
  }
@@ -302,8 +302,9 @@ export class EdgeFunctionsRegistry {
302
302
 
303
303
  /**
304
304
  * @param {string} urlPath
305
+ * @param {string} method
305
306
  */
306
- matchURLPath(urlPath) {
307
+ matchURLPath(urlPath, method) {
307
308
  const declarations = this.#bundler.mergeDeclarations(
308
309
  this.#declarationsFromTOML,
309
310
  this.#userFunctionConfigs,
@@ -318,23 +319,42 @@ export class EdgeFunctionsRegistry {
318
319
  functions: this.#functions,
319
320
  featureFlags,
320
321
  })
321
- const invocationMetadata = {
322
- function_config: manifest.function_config,
323
- routes: manifest.routes.map((route) => ({ function: route.function, pattern: route.pattern })),
324
- }
325
322
  const routes = [...manifest.routes, ...manifest.post_cache_routes].map((route) => ({
326
323
  ...route,
327
324
  pattern: new RegExp(route.pattern),
328
325
  }))
329
- const functionNames = routes
330
- .filter(({ pattern }) => pattern.test(urlPath))
331
- .filter(({ function: name }) => {
332
- const isExcluded = manifest.function_config[name]?.excluded_patterns?.some((pattern) =>
333
- new RegExp(pattern).test(urlPath),
334
- )
335
- return !isExcluded
336
- })
337
- .map((route) => route.function)
326
+
327
+ /** @type string[] */
328
+ const functionNames = []
329
+
330
+ /** @type number[] */
331
+ const routeIndexes = []
332
+
333
+ routes.forEach((route, index) => {
334
+ if (route.methods && route.methods.length !== 0 && !route.methods.includes(method)) {
335
+ return
336
+ }
337
+
338
+ if (!route.pattern.test(urlPath)) {
339
+ return
340
+ }
341
+
342
+ const isExcluded = manifest.function_config[route.function]?.excluded_patterns?.some((pattern) =>
343
+ new RegExp(pattern).test(urlPath),
344
+ )
345
+
346
+ if (isExcluded) {
347
+ return
348
+ }
349
+
350
+ functionNames.push(route.function)
351
+ routeIndexes.push(index)
352
+ })
353
+ const invocationMetadata = {
354
+ function_config: manifest.function_config,
355
+ req_routes: routeIndexes,
356
+ routes: manifest.routes.map((route) => ({ function: route.function, path: route.path, pattern: route.pattern })),
357
+ }
338
358
  const orphanedDeclarations = this.#matchURLPathAgainstOrphanedDeclarations(urlPath)
339
359
 
340
360
  return { functionNames, invocationMetadata, orphanedDeclarations }
@@ -158,6 +158,36 @@ export default class NetlifyFunction {
158
158
  }
159
159
  }
160
160
 
161
+ /**
162
+ * Matches all routes agains the incoming request. If a match is found, then the matched route is returned.
163
+ * @param {string} rawPath
164
+ * @param {string} method
165
+ * @returns matched route
166
+ */
167
+ async matchURLPath(rawPath, method) {
168
+ await this.buildQueue
169
+
170
+ const path = (rawPath.endsWith('/') ? rawPath.slice(0, -1) : rawPath).toLowerCase()
171
+ const { routes = [] } = this.buildData
172
+ return routes.find(({ expression, literal, methods }) => {
173
+ if (methods.length !== 0 && !methods.includes(method)) {
174
+ return false
175
+ }
176
+
177
+ if (literal !== undefined) {
178
+ return path === literal
179
+ }
180
+
181
+ if (expression !== undefined) {
182
+ const regex = new RegExp(expression)
183
+
184
+ return regex.test(path)
185
+ }
186
+
187
+ return false
188
+ })
189
+ }
190
+
161
191
  get url() {
162
192
  // This line fixes the issue here https://github.com/netlify/cli/issues/4116
163
193
  // Not sure why `settings.port` was used here nor does a valid reference exist.
@@ -122,6 +122,16 @@ export class FunctionsRegistry {
122
122
  return this.functions.get(name)
123
123
  }
124
124
 
125
+ async getFunctionForURLPath(urlPath, method) {
126
+ for (const func of this.functions.values()) {
127
+ const route = await func.matchURLPath(urlPath, method)
128
+
129
+ if (route) {
130
+ return { func, route }
131
+ }
132
+ }
133
+ }
134
+
125
135
  async registerFunction(name, funcBeforeHook) {
126
136
  const { runtime } = funcBeforeHook
127
137
 
@@ -58,6 +58,7 @@ const buildFunction = async ({
58
58
  includedFiles,
59
59
  inputs,
60
60
  path: functionPath,
61
+ routes,
61
62
  runtimeAPIVersion,
62
63
  schedule,
63
64
  } = await memoizedBuild({
@@ -81,7 +82,7 @@ const buildFunction = async ({
81
82
 
82
83
  clearFunctionsCache(targetDirectory)
83
84
 
84
- return { buildPath, includedFiles, runtimeAPIVersion, srcFiles, schedule }
85
+ return { buildPath, includedFiles, routes, runtimeAPIVersion, srcFiles, schedule }
85
86
  }
86
87
 
87
88
  /**
@@ -7,6 +7,7 @@ import jwtDecode from 'jwt-decode'
7
7
 
8
8
  import { NETLIFYDEVERR, NETLIFYDEVLOG, error as errorExit, log } from '../../utils/command-helpers.mjs'
9
9
  import { CLOCKWORK_USERAGENT, getFunctionsDistPath, getInternalFunctionsDir } from '../../utils/functions/index.mjs'
10
+ import { NFFunctionName, NFFunctionRoute } from '../../utils/headers.mjs'
10
11
  import { headers as efHeaders } from '../edge-functions/headers.mjs'
11
12
  import { getGeoLocation } from '../geo-location.mjs'
12
13
 
@@ -55,9 +56,22 @@ export const createHandler = function (options) {
55
56
  const { functionsRegistry } = options
56
57
 
57
58
  return async function handler(request, response) {
58
- // handle proxies without path re-writes (http-servr)
59
- const cleanPath = request.path.replace(/^\/.netlify\/(functions|builders)/, '')
60
- const functionName = cleanPath.split('/').find(Boolean)
59
+ // If these headers are set, it means we've already matched a function and we
60
+ // can just grab its name directly. We delete the header from the request
61
+ // because we don't want to expose it to user code.
62
+ let functionName = request.header(NFFunctionName)
63
+ delete request.headers[NFFunctionName]
64
+ const functionRoute = request.header(NFFunctionRoute)
65
+ delete request.headers[NFFunctionRoute]
66
+
67
+ // If we didn't match a function with a custom route, let's try to match
68
+ // using the fixed URL format.
69
+ if (!functionName) {
70
+ const cleanPath = request.path.replace(/^\/.netlify\/(functions|builders)/, '')
71
+
72
+ functionName = cleanPath.split('/').find(Boolean)
73
+ }
74
+
61
75
  const func = functionsRegistry.get(functionName)
62
76
 
63
77
  if (func === undefined) {
@@ -136,6 +150,7 @@ export const createHandler = function (options) {
136
150
  isBase64Encoded,
137
151
  rawUrl,
138
152
  rawQuery,
153
+ route: functionRoute,
139
154
  }
140
155
 
141
156
  const clientContext = buildClientContext(request.headers) || {}
@@ -231,7 +246,7 @@ const getFunctionsServer = (options) => {
231
246
  * @param {*} options.site
232
247
  * @param {string} options.siteUrl
233
248
  * @param {*} options.timeouts
234
- * @returns
249
+ * @returns {Promise<import('./registry.mjs').FunctionsRegistry | undefined>}
235
250
  */
236
251
  export const startFunctionsServer = async (options) => {
237
252
  const { capabilities, command, config, debug, loadDistFunctions, settings, site, siteUrl, timeouts } = options
@@ -272,9 +287,11 @@ export const startFunctionsServer = async (options) => {
272
287
 
273
288
  await functionsRegistry.scan(functionsDirectories)
274
289
 
275
- const server = await getFunctionsServer(Object.assign(options, { functionsRegistry }))
290
+ const server = getFunctionsServer(Object.assign(options, { functionsRegistry }))
276
291
 
277
292
  await startWebServer({ server, settings, debug })
293
+
294
+ return functionsRegistry
278
295
  }
279
296
 
280
297
  /**
@@ -46,4 +46,6 @@ const getErrorMessage = function ({ message }) {
46
46
  return message
47
47
  }
48
48
 
49
+ export const NFFunctionName = 'x-nf-function-name'
50
+ export const NFFunctionRoute = 'x-nf-function-route'
49
51
  export const NFRequestID = 'x-nf-request-id'
@@ -52,6 +52,7 @@ export const generateInspectSettings = (edgeInspect, edgeInspectBrk) => {
52
52
  * @param {*} params.siteInfo
53
53
  * @param {string} params.projectDir
54
54
  * @param {import('./state-config.mjs').default} params.state
55
+ * @param {import('../lib/functions/registry.mjs').FunctionsRegistry=} params.functionsRegistry
55
56
  * @returns
56
57
  */
57
58
  export const startProxyServer = async ({
@@ -61,6 +62,7 @@ export const startProxyServer = async ({
61
62
  configPath,
62
63
  debug,
63
64
  env,
65
+ functionsRegistry,
64
66
  geoCountry,
65
67
  geolocationMode,
66
68
  getUpdatedConfig,
@@ -78,6 +80,7 @@ export const startProxyServer = async ({
78
80
  configPath: configPath || site.configPath,
79
81
  debug,
80
82
  env,
83
+ functionsRegistry,
81
84
  geolocationMode,
82
85
  geoCountry,
83
86
  getUpdatedConfig,
@@ -31,7 +31,7 @@ import renderErrorTemplate from '../lib/render-error-template.mjs'
31
31
 
32
32
  import { NETLIFYDEVLOG, NETLIFYDEVWARN, log, chalk } from './command-helpers.mjs'
33
33
  import createStreamPromise from './create-stream-promise.mjs'
34
- import { headersForPath, parseHeaders, NFRequestID } from './headers.mjs'
34
+ import { headersForPath, parseHeaders, NFFunctionName, NFRequestID, NFFunctionRoute } from './headers.mjs'
35
35
  import { generateRequestID } from './request-id.mjs'
36
36
  import { createRewriter, onChanges } from './rules-proxy.mjs'
37
37
  import { signRedirect } from './sign-redirect.mjs'
@@ -181,7 +181,7 @@ const alternativePathsFor = function (url) {
181
181
  return paths
182
182
  }
183
183
 
184
- const serveRedirect = async function ({ env, match, options, proxy, req, res, siteInfo }) {
184
+ const serveRedirect = async function ({ env, functionsRegistry, match, options, proxy, req, res, siteInfo }) {
185
185
  if (!match) return proxy.web(req, res, options)
186
186
 
187
187
  options = options || req.proxyOptions || {}
@@ -214,6 +214,7 @@ const serveRedirect = async function ({ env, match, options, proxy, req, res, si
214
214
  if (isFunction(options.functionsPort, req.url)) {
215
215
  return proxy.web(req, res, { target: options.functionsServer })
216
216
  }
217
+
217
218
  const urlForAddons = getAddonUrl(options.addonsUrls, req)
218
219
  if (urlForAddons) {
219
220
  return handleAddonUrl({ req, res, addonUrl: urlForAddons })
@@ -327,22 +328,31 @@ const serveRedirect = async function ({ env, match, options, proxy, req, res, si
327
328
  return proxy.web(req, res, { target: options.functionsServer })
328
329
  }
329
330
 
331
+ const functionWithCustomRoute =
332
+ functionsRegistry && (await functionsRegistry.getFunctionForURLPath(destURL, req.method))
330
333
  const destStaticFile = await getStatic(dest.pathname, options.publicFolder)
331
334
  let statusValue
332
- if (match.force || (!staticFile && ((!options.framework && destStaticFile) || isInternal(destURL)))) {
335
+ if (
336
+ match.force ||
337
+ (!staticFile && ((!options.framework && destStaticFile) || isInternal(destURL) || functionWithCustomRoute))
338
+ ) {
333
339
  req.url = destStaticFile ? destStaticFile + dest.search : destURL
334
340
  const { status } = match
335
341
  statusValue = status
336
342
  console.log(`${NETLIFYDEVLOG} Rewrote URL to`, req.url)
337
343
  }
338
344
 
339
- if (isFunction(options.functionsPort, req.url)) {
345
+ if (isFunction(options.functionsPort, req.url) || functionWithCustomRoute) {
346
+ const functionHeaders = functionWithCustomRoute
347
+ ? { [NFFunctionName]: functionWithCustomRoute.func.name, [NFFunctionRoute]: functionWithCustomRoute.route }
348
+ : {}
340
349
  const url = reqToURL(req, originalURL)
341
350
  req.headers['x-netlify-original-pathname'] = url.pathname
342
351
  req.headers['x-netlify-original-search'] = url.search
343
352
 
344
- return proxy.web(req, res, { target: options.functionsServer })
353
+ return proxy.web(req, res, { headers: functionHeaders, target: options.functionsServer })
345
354
  }
355
+
346
356
  const addonUrl = getAddonUrl(options.addonsUrls, req)
347
357
  if (addonUrl) {
348
358
  return handleAddonUrl({ req, res, addonUrl })
@@ -434,12 +444,22 @@ const initializeProxy = async function ({ configPath, distDir, env, host, port,
434
444
  }
435
445
 
436
446
  if (proxyRes.statusCode === 404 || proxyRes.statusCode === 403) {
447
+ // If a request for `/path` has failed, we'll a few variations like
448
+ // `/path/index.html` to mimic the CDN behavior.
437
449
  if (req.alternativePaths && req.alternativePaths.length !== 0) {
438
450
  req.url = req.alternativePaths.shift()
439
451
  return proxy.web(req, res, req.proxyOptions)
440
452
  }
453
+
454
+ // The request has failed but we might still have a matching redirect
455
+ // rule (without `force`) that should kick in. This is how we mimic the
456
+ // file shadowing behavior from the CDN.
441
457
  if (req.proxyOptions && req.proxyOptions.match) {
442
458
  return serveRedirect({
459
+ // We don't want to match functions at this point because any redirects
460
+ // to functions will have already been processed, so we don't supply a
461
+ // functions registry to `serveRedirect`.
462
+ functionsRegistry: null,
443
463
  req,
444
464
  res,
445
465
  proxy: handlers,
@@ -453,7 +473,19 @@ const initializeProxy = async function ({ configPath, distDir, env, host, port,
453
473
 
454
474
  if (req.proxyOptions.staticFile && isRedirect({ status: proxyRes.statusCode }) && proxyRes.headers.location) {
455
475
  req.url = proxyRes.headers.location
456
- return serveRedirect({ req, res, proxy: handlers, match: null, options: req.proxyOptions, siteInfo, env })
476
+ return serveRedirect({
477
+ // We don't want to match functions at this point because any redirects
478
+ // to functions will have already been processed, so we don't supply a
479
+ // functions registry to `serveRedirect`.
480
+ functionsRegistry: null,
481
+ req,
482
+ res,
483
+ proxy: handlers,
484
+ match: null,
485
+ options: req.proxyOptions,
486
+ siteInfo,
487
+ env,
488
+ })
457
489
  }
458
490
 
459
491
  const responseData = []
@@ -551,7 +583,7 @@ const initializeProxy = async function ({ configPath, distDir, env, host, port,
551
583
  }
552
584
 
553
585
  const onRequest = async (
554
- { addonsUrls, edgeFunctionsProxy, env, functionsServer, proxy, rewriter, settings, siteInfo },
586
+ { addonsUrls, edgeFunctionsProxy, env, functionsRegistry, functionsServer, proxy, rewriter, settings, siteInfo },
555
587
  req,
556
588
  res,
557
589
  ) => {
@@ -565,9 +597,22 @@ const onRequest = async (
565
597
  return proxy.web(req, res, { target: edgeFunctionsProxyURL })
566
598
  }
567
599
 
600
+ // Does the request match a function on the fixed URL path?
568
601
  if (isFunction(settings.functionsPort, req.url)) {
569
602
  return proxy.web(req, res, { target: functionsServer })
570
603
  }
604
+
605
+ // Does the request match a function on a custom URL path?
606
+ const functionMatch = functionsRegistry ? await functionsRegistry.getFunctionForURLPath(req.url, req.method) : null
607
+
608
+ if (functionMatch) {
609
+ // Setting an internal header with the function name so that we don't
610
+ // have to match the URL again in the functions server.
611
+ const headers = { [NFFunctionName]: functionMatch.func.name, [NFFunctionRoute]: functionMatch.route.pattern }
612
+
613
+ return proxy.web(req, res, { headers, target: functionsServer })
614
+ }
615
+
571
616
  const addonUrl = getAddonUrl(addonsUrls, req)
572
617
  if (addonUrl) {
573
618
  return handleAddonUrl({ req, res, addonUrl })
@@ -591,7 +636,7 @@ const onRequest = async (
591
636
  // We don't want to generate an ETag for 3xx redirects.
592
637
  req[shouldGenerateETag] = ({ statusCode }) => statusCode < 300 || statusCode >= 400
593
638
 
594
- return serveRedirect({ req, res, proxy, match, options, siteInfo, env })
639
+ return serveRedirect({ req, res, proxy, match, options, siteInfo, env, functionsRegistry })
595
640
  }
596
641
 
597
642
  // The request will be served by the framework server, which means we want to
@@ -628,6 +673,7 @@ export const startProxy = async function ({
628
673
  configPath,
629
674
  debug,
630
675
  env,
676
+ functionsRegistry,
631
677
  geoCountry,
632
678
  geolocationMode,
633
679
  getUpdatedConfig,
@@ -652,6 +698,7 @@ export const startProxy = async function ({
652
698
  mainPort: settings.port,
653
699
  offline,
654
700
  passthroughPort: secondaryServerPort || settings.port,
701
+ settings,
655
702
  projectDir,
656
703
  siteInfo,
657
704
  accountId,
@@ -681,6 +728,7 @@ export const startProxy = async function ({
681
728
  rewriter,
682
729
  settings,
683
730
  addonsUrls,
731
+ functionsRegistry,
684
732
  functionsServer,
685
733
  edgeFunctionsProxy,
686
734
  siteInfo,