Repository: netlify/cli
Pull request #5782 (2023-06-07 17:31:47)

fix: expose original URL search to function redirects
#### Summary

Closes https://github.com/netlify/pillar-support/issues/447.

github-actions[bot]: ## 📊 Benchmark results

- **Dependency count**: 1,228
- **Package size**: 238 MB
<!--delta:post-package-size@{}-->

Patch:
diff --git a/src/lib/functions/form-submissions-handler.mjs b/src/lib/functions/form-submissions-handler.mjs
index c9258ddc..8c5f5169 100644
--- a/src/lib/functions/form-submissions-handler.mjs
+++ b/src/lib/functions/form-submissions-handler.mjs
@@ -139,6 +139,7 @@ export const createFormSubmissionHandler = function ({ functionsRegistry, siteUr
       'content-length': data.length,
       'content-type': 'application/json',
       'x-netlify-original-pathname': originalUrl.pathname,
+      'x-netlify-original-search': originalUrl.search,
     }
 
     next()
diff --git a/src/lib/functions/server.mjs b/src/lib/functions/server.mjs
index 2840d9fd..1ac9d951 100644
--- a/src/lib/functions/server.mjs
+++ b/src/lib/functions/server.mjs
@@ -84,7 +84,14 @@ export const createHandler = function (options) {
       requestPath = request.header('x-netlify-original-pathname')
       delete request.headers['x-netlify-original-pathname']
     }
-    const queryParams = Object.entries(request.query).reduce(
+
+    let requestQuery = request.query
+    if (request.header('x-netlify-original-search')) {
+      requestQuery = Object.fromEntries(new URLSearchParams(request.header('x-netlify-original-search')))
+      delete request.headers['x-netlify-original-search']
+    }
+
+    const queryParams = Object.entries(requestQuery).reduce(
       (prev, [key, value]) => ({ ...prev, [key]: Array.isArray(value) ? value : [value] }),
       {},
     )
@@ -92,7 +99,7 @@ export const createHandler = function (options) {
       (prev, [key, value]) => ({ ...prev, [key]: Array.isArray(value) ? value : [value] }),
       {},
     )
-    const rawQuery = new URLSearchParams(request.query).toString()
+    const rawQuery = new URLSearchParams(requestQuery).toString()
     const protocol = options.config?.dev?.https ? 'https' : 'http'
     const url = new URL(requestPath, `${protocol}://${request.get('host') || 'localhost'}`)
     url.search = rawQuery
diff --git a/src/utils/proxy.mjs b/src/utils/proxy.mjs
index 0f093fb0..6bed105a 100644
--- a/src/utils/proxy.mjs
+++ b/src/utils/proxy.mjs
@@ -301,7 +301,10 @@ const serveRedirect = async function ({ env, match, options, proxy, req, res, si
     }
 
     if (isFunction(options.functionsPort, req.url)) {
-      req.headers['x-netlify-original-pathname'] = reqToURL(req, originalURL).pathname
+      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 })
     }
     const addonUrl = getAddonUrl(options.addonsUrls, req)


Test patch:
diff --git a/tests/integration/__fixtures__/dev-server-with-functions/functions/echo.js b/tests/integration/__fixtures__/dev-server-with-functions/functions/echo.js
new file mode 100644
index 00000000..be5797a0
--- /dev/null
+++ b/tests/integration/__fixtures__/dev-server-with-functions/functions/echo.js
@@ -0,0 +1,4 @@
+exports.handler = async (event) => ({
+  statusCode: 200,
+  body: JSON.stringify(event),
+})
diff --git a/tests/integration/__fixtures__/dev-server-with-functions/netlify.toml b/tests/integration/__fixtures__/dev-server-with-functions/netlify.toml
index a384e113..83c1a031 100644
--- a/tests/integration/__fixtures__/dev-server-with-functions/netlify.toml
+++ b/tests/integration/__fixtures__/dev-server-with-functions/netlify.toml
@@ -3,3 +3,14 @@ directory = "functions"
 
 [functions.scheduled]
 schedule = '@daily'
+
+[[redirects]]
+from = "/api/*"
+to = "/.netlify/functions/:splat"
+status = 200
+
+[[redirects]]
+from = "/with-params"
+to = "/.netlify/functions/echo?param1=hello&param2=:param2"
+status = 200
+query = { param2 = ":param2" }
diff --git a/tests/integration/commands/dev/redirects.test.ts b/tests/integration/commands/dev/redirects.test.ts
new file mode 100644
index 00000000..efda99de
--- /dev/null
+++ b/tests/integration/commands/dev/redirects.test.ts
@@ -0,0 +1,22 @@
+import { describe, expect, test } from 'vitest'
+
+import { FixtureTestContext, setupFixtureTests } from '../../utils/fixture.js'
+import got from '../../utils/got.cjs'
+
+describe('redirects', () => {
+  setupFixtureTests('dev-server-with-functions', { devServer: true }, () => {
+    test<FixtureTestContext>('should send original query params to functions', async ({ devServer }) => {
+      const response = await got(`http://localhost:${devServer.port}/with-params?param2=world&other=1`, {
+        throwHttpErrors: false,
+        retry: { limit: 0 },
+      })
+
+      expect(response.statusCode).toBe(200)
+
+      const result = JSON.parse(response.body)
+      expect(result.queryStringParameters).not.toHaveProperty('param1')
+      expect(result.queryStringParameters).toHaveProperty('param2', 'world')
+      expect(result.queryStringParameters).toHaveProperty('other', '1')
+    })
+  })
+})