Skip to content

Commit

Permalink
Helpers update (#7686)
Browse files Browse the repository at this point in the history
* 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 <jj@jjsweb.site>
  • Loading branch information
huv1k and ijjk authored Jun 28, 2019
1 parent ec958a3 commit c156e0c
Show file tree
Hide file tree
Showing 8 changed files with 146 additions and 28 deletions.
6 changes: 0 additions & 6 deletions examples/with-webassembly/Cargo.toml

This file was deleted.

84 changes: 68 additions & 16 deletions packages/next-server/server/api-utils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,12 +2,15 @@ import { IncomingMessage } from 'http'
import { NextApiResponse, NextApiRequest } from '../lib/utils'
import { Stream } from 'stream'
import getRawBody from 'raw-body'
import { URL } from 'url'
import { parse } from 'content-type'
import { Params } from './router'

export type NextApiRequestCookies = { [key: string]: string }
export type NextApiRequestQuery = { [key: string]: string | string[] }

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

return reduceParams(params.entries())
} else {
return {}
const query: { [key: string]: string | string[] } = {}
for (const [key, value] of params) {
query[key] = value
}

return query
}
}

/**
* Parse cookeies from `req` header
* @param req request object
*/
export function getCookieParser(req: IncomingMessage) {
return function parseCookie(): NextApiRequestCookies {
const header: undefined | string | string[] = req.headers.cookie

if (!header) {
return {}
}

const { parse } = require('cookie')
return parse(Array.isArray(header) ? header.join(';') : header)
}
}

Expand Down Expand Up @@ -131,14 +155,6 @@ export function sendJson(res: NextApiResponse, jsonBody: any): void {
res.send(jsonBody)
}

function reduceParams(params: IterableIterator<[string, string]>) {
const obj: any = {}
for (const [key, value] of params) {
obj[key] = value
}
return obj
}

/**
* Custom error class
*/
Expand Down Expand Up @@ -166,3 +182,39 @@ export function sendError(
res.statusMessage = message
res.end()
}

interface LazyProps {
req: NextApiRequest
params?: Params | boolean
}

/**
* Execute getter function only if its needed
* @param LazyProps `req` and `params` for lazyProp
* @param prop name of property
* @param getter function to get data
*/
export function setLazyProp<T>(
{ req, params }: LazyProps,
prop: string,
getter: () => T
) {
const opts = { configurable: true, enumerable: true }
const optsReset = { ...opts, writable: true }

Object.defineProperty(req, prop, {
...opts,
get: () => {
let value = getter()
if (params && typeof params !== 'boolean') {
value = { ...value, ...params }
}
// we set the property on the object to avoid recalculating it
Object.defineProperty(req, prop, { ...optsReset, value })
return value
},
set: value => {
Object.defineProperty(req, prop, { ...optsReset, value })
},
})
}
3 changes: 3 additions & 0 deletions packages/next-server/server/load-components.ts
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,9 @@ export function interopDefault(mod: any) {

export interface IPageConfig {
amp?: boolean | 'hybrid'
api?: {
bodyParser?: boolean
}
}

export type LoadComponentsReturnType = {
Expand Down
25 changes: 19 additions & 6 deletions packages/next-server/server/next-server.ts
Original file line number Diff line number Diff line change
Expand Up @@ -23,22 +23,24 @@ import {
} from '../lib/router/utils'
import * as envConfig from '../lib/runtime-config'
import { NextApiRequest, NextApiResponse } from '../lib/utils'
import { parse as parseCookies } from 'cookie'
import {
parseQuery,
getQueryParser,
sendJson,
sendData,
parseBody,
sendError,
ApiError,
sendStatusCode,
setLazyProp,
getCookieParser,
} from './api-utils'
import loadConfig from './config'
import { recursiveReadDirSync } from './lib/recursive-readdir-sync'
import {
interopDefault,
loadComponents,
LoadComponentsReturnType,
IPageConfig,
} from './load-components'
import { renderToHTML } from './render'
import { getPagePath } from './require'
Expand Down Expand Up @@ -289,6 +291,7 @@ export default class Server {
res: NextApiResponse,
pathname: string
) {
let bodyParser = true
let params: Params | boolean = false

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

try {
const resolverModule = require(resolverFunction)

if (resolverModule.config) {
const config: IPageConfig = resolverModule.config
if (config.api && config.api.bodyParser === false) {
bodyParser = false
}
}
// Parsing of cookies
req.cookies = parseCookies(req.headers.cookie || '')
setLazyProp({ req }, 'cookies', getCookieParser(req))
// Parsing query string
req.query = { ...parseQuery(req), ...params }
setLazyProp({ req, params }, 'query', getQueryParser(req))
// // Parsing of body
req.body = await parseBody(req)
if (bodyParser) {
req.body = await parseBody(req)
}

res.status = statusCode => sendStatusCode(res, statusCode)
res.send = data => sendData(res, data)
res.json = data => sendJson(res, data)

const resolver = interopDefault(require(resolverFunction))
const resolver = interopDefault(resolverModule)
resolver(req, res)
} catch (e) {
if (e instanceof ApiError) {
Expand Down
3 changes: 3 additions & 0 deletions packages/next/types/index.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -48,6 +48,9 @@ export type NextPage<P = {}, IP = P> = {
*/
export type PageConfig = {
amp?: boolean | 'hybrid'
api?: {
bodyParser?: boolean
}
}

export { NextPageContext, NextComponentType, NextApiResponse, NextApiRequest }
18 changes: 18 additions & 0 deletions test/integration/api-support/pages/api/no-parsing.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
export const config = {
api: {
bodyParser: false
}
}

export default (req, res) => {
if (!req.body) {
let buffer = ''
req.on('data', chunk => {
buffer += chunk
})

req.on('end', () => {
res.status(200).json(JSON.parse(Buffer.from(buffer).toString()))
})
}
}
11 changes: 11 additions & 0 deletions test/integration/api-support/pages/api/parsing.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
export const config = {
api: {
bodyParser: true
}
}

export default (req, res) => {
if (req.body) {
res.status(200).json({ message: 'Parsed body' })
}
}
24 changes: 24 additions & 0 deletions test/integration/api-support/test/index.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -103,6 +103,30 @@ function runTests (serverless = false) {
})
})

it('should parse body in handler', async () => {
const data = await fetchViaHTTP(appPort, '/api/no-parsing', null, {
method: 'POST',
headers: {
'Content-Type': 'application/json; charset=utf-8'
},
body: JSON.stringify([{ title: 'Nextjs' }])
}).then(res => res.ok && res.json())

expect(data).toEqual([{ title: 'Nextjs' }])
})

it('should parse body with config', async () => {
const data = await fetchViaHTTP(appPort, '/api/parsing', null, {
method: 'POST',
headers: {
'Content-Type': 'application/json; charset=utf-8'
},
body: JSON.stringify([{ title: 'Nextjs' }])
}).then(res => res.ok && res.json())

expect(data).toEqual({ message: 'Parsed body' })
})

it('should return empty cookies object', async () => {
const data = await fetchViaHTTP(appPort, '/api/cookies', null, {}).then(
res => res.ok && res.json()
Expand Down

0 comments on commit c156e0c

Please sign in to comment.