Skip to content

Commit c156e0c

Browse files
huv1kijjk
andauthored
Helpers update (#7686)
* Update helper function to invoke only on get * Tests for body parsing * Update api-utils.ts * Update next-server.ts * Update packages/next-server/server/next-server.ts Co-Authored-By: JJ Kasper <[email protected]>
1 parent ec958a3 commit c156e0c

File tree

8 files changed

+146
-28
lines changed

8 files changed

+146
-28
lines changed

examples/with-webassembly/Cargo.toml

-6
This file was deleted.

packages/next-server/server/api-utils.ts

+68-16
Original file line numberDiff line numberDiff line change
@@ -2,12 +2,15 @@ import { IncomingMessage } from 'http'
22
import { NextApiResponse, NextApiRequest } from '../lib/utils'
33
import { Stream } from 'stream'
44
import getRawBody from 'raw-body'
5-
import { URL } from 'url'
65
import { parse } from 'content-type'
6+
import { Params } from './router'
7+
8+
export type NextApiRequestCookies = { [key: string]: string }
9+
export type NextApiRequestQuery = { [key: string]: string | string[] }
710

811
/**
912
* Parse incoming message like `json` or `urlencoded`
10-
* @param req
13+
* @param req request object
1114
*/
1215
export async function parseBody(req: NextApiRequest, limit: string = '1mb') {
1316
const contentType = parse(req.headers['content-type'] || 'text/plain')
@@ -55,14 +58,35 @@ function parseJson(str: string) {
5558
* @param url of request
5659
* @returns Object with key name of query argument and its value
5760
*/
58-
export function parseQuery({ url }: IncomingMessage) {
59-
if (url) {
60-
// This is just for parsing search params, base it's not important
61+
export function getQueryParser({ url }: IncomingMessage) {
62+
return function parseQuery(): NextApiRequestQuery {
63+
const { URL } = require('url')
64+
// we provide a placeholder base url because we only want searchParams
6165
const params = new URL(url, 'https://n').searchParams
6266

63-
return reduceParams(params.entries())
64-
} else {
65-
return {}
67+
const query: { [key: string]: string | string[] } = {}
68+
for (const [key, value] of params) {
69+
query[key] = value
70+
}
71+
72+
return query
73+
}
74+
}
75+
76+
/**
77+
* Parse cookeies from `req` header
78+
* @param req request object
79+
*/
80+
export function getCookieParser(req: IncomingMessage) {
81+
return function parseCookie(): NextApiRequestCookies {
82+
const header: undefined | string | string[] = req.headers.cookie
83+
84+
if (!header) {
85+
return {}
86+
}
87+
88+
const { parse } = require('cookie')
89+
return parse(Array.isArray(header) ? header.join(';') : header)
6690
}
6791
}
6892

@@ -131,14 +155,6 @@ export function sendJson(res: NextApiResponse, jsonBody: any): void {
131155
res.send(jsonBody)
132156
}
133157

134-
function reduceParams(params: IterableIterator<[string, string]>) {
135-
const obj: any = {}
136-
for (const [key, value] of params) {
137-
obj[key] = value
138-
}
139-
return obj
140-
}
141-
142158
/**
143159
* Custom error class
144160
*/
@@ -166,3 +182,39 @@ export function sendError(
166182
res.statusMessage = message
167183
res.end()
168184
}
185+
186+
interface LazyProps {
187+
req: NextApiRequest
188+
params?: Params | boolean
189+
}
190+
191+
/**
192+
* Execute getter function only if its needed
193+
* @param LazyProps `req` and `params` for lazyProp
194+
* @param prop name of property
195+
* @param getter function to get data
196+
*/
197+
export function setLazyProp<T>(
198+
{ req, params }: LazyProps,
199+
prop: string,
200+
getter: () => T
201+
) {
202+
const opts = { configurable: true, enumerable: true }
203+
const optsReset = { ...opts, writable: true }
204+
205+
Object.defineProperty(req, prop, {
206+
...opts,
207+
get: () => {
208+
let value = getter()
209+
if (params && typeof params !== 'boolean') {
210+
value = { ...value, ...params }
211+
}
212+
// we set the property on the object to avoid recalculating it
213+
Object.defineProperty(req, prop, { ...optsReset, value })
214+
return value
215+
},
216+
set: value => {
217+
Object.defineProperty(req, prop, { ...optsReset, value })
218+
},
219+
})
220+
}

packages/next-server/server/load-components.ts

+3
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,9 @@ export function interopDefault(mod: any) {
1414

1515
export interface IPageConfig {
1616
amp?: boolean | 'hybrid'
17+
api?: {
18+
bodyParser?: boolean
19+
}
1720
}
1821

1922
export type LoadComponentsReturnType = {

packages/next-server/server/next-server.ts

+19-6
Original file line numberDiff line numberDiff line change
@@ -23,22 +23,24 @@ import {
2323
} from '../lib/router/utils'
2424
import * as envConfig from '../lib/runtime-config'
2525
import { NextApiRequest, NextApiResponse } from '../lib/utils'
26-
import { parse as parseCookies } from 'cookie'
2726
import {
28-
parseQuery,
27+
getQueryParser,
2928
sendJson,
3029
sendData,
3130
parseBody,
3231
sendError,
3332
ApiError,
3433
sendStatusCode,
34+
setLazyProp,
35+
getCookieParser,
3536
} from './api-utils'
3637
import loadConfig from './config'
3738
import { recursiveReadDirSync } from './lib/recursive-readdir-sync'
3839
import {
3940
interopDefault,
4041
loadComponents,
4142
LoadComponentsReturnType,
43+
IPageConfig,
4244
} from './load-components'
4345
import { renderToHTML } from './render'
4446
import { getPagePath } from './require'
@@ -289,6 +291,7 @@ export default class Server {
289291
res: NextApiResponse,
290292
pathname: string
291293
) {
294+
let bodyParser = true
292295
let params: Params | boolean = false
293296

294297
let resolverFunction = await this.resolveApiRequest(pathname)
@@ -313,18 +316,28 @@ export default class Server {
313316
}
314317

315318
try {
319+
const resolverModule = require(resolverFunction)
320+
321+
if (resolverModule.config) {
322+
const config: IPageConfig = resolverModule.config
323+
if (config.api && config.api.bodyParser === false) {
324+
bodyParser = false
325+
}
326+
}
316327
// Parsing of cookies
317-
req.cookies = parseCookies(req.headers.cookie || '')
328+
setLazyProp({ req }, 'cookies', getCookieParser(req))
318329
// Parsing query string
319-
req.query = { ...parseQuery(req), ...params }
330+
setLazyProp({ req, params }, 'query', getQueryParser(req))
320331
// // Parsing of body
321-
req.body = await parseBody(req)
332+
if (bodyParser) {
333+
req.body = await parseBody(req)
334+
}
322335

323336
res.status = statusCode => sendStatusCode(res, statusCode)
324337
res.send = data => sendData(res, data)
325338
res.json = data => sendJson(res, data)
326339

327-
const resolver = interopDefault(require(resolverFunction))
340+
const resolver = interopDefault(resolverModule)
328341
resolver(req, res)
329342
} catch (e) {
330343
if (e instanceof ApiError) {

packages/next/types/index.d.ts

+3
Original file line numberDiff line numberDiff line change
@@ -48,6 +48,9 @@ export type NextPage<P = {}, IP = P> = {
4848
*/
4949
export type PageConfig = {
5050
amp?: boolean | 'hybrid'
51+
api?: {
52+
bodyParser?: boolean
53+
}
5154
}
5255

5356
export { NextPageContext, NextComponentType, NextApiResponse, NextApiRequest }
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,18 @@
1+
export const config = {
2+
api: {
3+
bodyParser: false
4+
}
5+
}
6+
7+
export default (req, res) => {
8+
if (!req.body) {
9+
let buffer = ''
10+
req.on('data', chunk => {
11+
buffer += chunk
12+
})
13+
14+
req.on('end', () => {
15+
res.status(200).json(JSON.parse(Buffer.from(buffer).toString()))
16+
})
17+
}
18+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,11 @@
1+
export const config = {
2+
api: {
3+
bodyParser: true
4+
}
5+
}
6+
7+
export default (req, res) => {
8+
if (req.body) {
9+
res.status(200).json({ message: 'Parsed body' })
10+
}
11+
}

test/integration/api-support/test/index.test.js

+24
Original file line numberDiff line numberDiff line change
@@ -103,6 +103,30 @@ function runTests (serverless = false) {
103103
})
104104
})
105105

106+
it('should parse body in handler', async () => {
107+
const data = await fetchViaHTTP(appPort, '/api/no-parsing', null, {
108+
method: 'POST',
109+
headers: {
110+
'Content-Type': 'application/json; charset=utf-8'
111+
},
112+
body: JSON.stringify([{ title: 'Nextjs' }])
113+
}).then(res => res.ok && res.json())
114+
115+
expect(data).toEqual([{ title: 'Nextjs' }])
116+
})
117+
118+
it('should parse body with config', async () => {
119+
const data = await fetchViaHTTP(appPort, '/api/parsing', null, {
120+
method: 'POST',
121+
headers: {
122+
'Content-Type': 'application/json; charset=utf-8'
123+
},
124+
body: JSON.stringify([{ title: 'Nextjs' }])
125+
}).then(res => res.ok && res.json())
126+
127+
expect(data).toEqual({ message: 'Parsed body' })
128+
})
129+
106130
it('should return empty cookies object', async () => {
107131
const data = await fetchViaHTTP(appPort, '/api/cookies', null, {}).then(
108132
res => res.ok && res.json()

0 commit comments

Comments
 (0)