diff --git a/packages/next/src/server/api-utils/node/api-resolver.ts b/packages/next/src/server/api-utils/node/api-resolver.ts index c5a563432da737..aab866f2bbdc01 100644 --- a/packages/next/src/server/api-utils/node/api-resolver.ts +++ b/packages/next/src/server/api-utils/node/api-resolver.ts @@ -355,8 +355,14 @@ export async function apiResolver( // Parsing of cookies setLazyProp({ req: apiReq }, 'cookies', getCookieParser(req.headers)) - // Parsing query string - apiReq.query = query + // Ensure req.query is a writable, enumerable property by using Object.defineProperty. + // This addresses Express 5.x, which defines query as a getter only (read-only). + Object.defineProperty(apiReq, 'query', { + value: { ...query }, + writable: true, + enumerable: true, + configurable: true, + }) // Parsing preview data setLazyProp({ req: apiReq }, 'previewData', () => tryGetPreviewData(req, res, apiContext, !!apiContext.multiZoneDraftMode) diff --git a/test/e2e/api-resolver-query-writeable/api-resolver-query-writeable.test.ts b/test/e2e/api-resolver-query-writeable/api-resolver-query-writeable.test.ts new file mode 100644 index 00000000000000..19a56e9db80be5 --- /dev/null +++ b/test/e2e/api-resolver-query-writeable/api-resolver-query-writeable.test.ts @@ -0,0 +1,31 @@ +import { nextTestSetup } from 'e2e-utils' + +describe('api-resolver-query-writeable', () => { + const { next, skipped } = nextTestSetup({ + files: __dirname, + skipDeployment: true, + startCommand: 'node server.js', + serverReadyPattern: /Next mode: (production|development)/, + dependencies: { + 'get-port': '5.1.1', + express: '5.1.0', + }, + }) + + if (skipped) { + return + } + + it('should allow req.query to be writable and reflect changes made in the API handler', async () => { + const res = await next.fetch('/api?hello=yes', { + headers: { + 'Content-Type': 'application/json; charset=utf-8', + }, + }) + if (!res.ok) { + throw new Error('Fetch failed') + } + const data = await res.json() + expect(data).toEqual({ query: { hello: 'yes', changed: 'yes' } }) + }) +}) diff --git a/test/e2e/api-resolver-query-writeable/pages/api/index.js b/test/e2e/api-resolver-query-writeable/pages/api/index.js new file mode 100644 index 00000000000000..80909dde278a1e --- /dev/null +++ b/test/e2e/api-resolver-query-writeable/pages/api/index.js @@ -0,0 +1,4 @@ +export default (req, res) => { + req.query = { ...req.query, changed: 'yes' } + res.status(200).json({ query: req.query }) +} diff --git a/test/e2e/api-resolver-query-writeable/server.js b/test/e2e/api-resolver-query-writeable/server.js new file mode 100644 index 00000000000000..060d67e5706cdf --- /dev/null +++ b/test/e2e/api-resolver-query-writeable/server.js @@ -0,0 +1,35 @@ +const next = require('next') +const express = require('express') + +const getPort = require('get-port') + +async function main() { + const dev = process.env.NEXT_TEST_MODE === 'dev' + process.env.NODE_ENV = dev ? 'development' : 'production' + + const port = await getPort() + const app = next({ dev }) + const handleNextRequest = app.getRequestHandler() + + await app.prepare() + + const server = express() + + server.all('/:path', (req, res) => { + handleNextRequest(req, res) + }) + + server.listen(port, (err) => { + if (err) { + throw err + } + + console.log(`- Local: http://localhost:${port}`) + console.log(`- Next mode: ${dev ? 'development' : process.env.NODE_ENV}`) + }) +} + +main().catch((err) => { + console.error(err) + process.exit(1) +})