From c156e0c88918d8354d712a15069f2ab0f4b427bd Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Luk=C3=A1=C5=A1=20Huvar?= Date: Fri, 28 Jun 2019 11:31:32 +0200 Subject: [PATCH] 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 --- examples/with-webassembly/Cargo.toml | 6 -- packages/next-server/server/api-utils.ts | 84 +++++++++++++++---- .../next-server/server/load-components.ts | 3 + packages/next-server/server/next-server.ts | 25 ++++-- packages/next/types/index.d.ts | 3 + .../api-support/pages/api/no-parsing.js | 18 ++++ .../api-support/pages/api/parsing.js | 11 +++ .../api-support/test/index.test.js | 24 ++++++ 8 files changed, 146 insertions(+), 28 deletions(-) delete mode 100644 examples/with-webassembly/Cargo.toml create mode 100644 test/integration/api-support/pages/api/no-parsing.js create mode 100644 test/integration/api-support/pages/api/parsing.js diff --git a/examples/with-webassembly/Cargo.toml b/examples/with-webassembly/Cargo.toml deleted file mode 100644 index 0c537fa46f37..000000000000 --- a/examples/with-webassembly/Cargo.toml +++ /dev/null @@ -1,6 +0,0 @@ -[package] -name = "next-rust" -version = "0.1.0" -authors = ["Tim Neutkens "] - -[dependencies] diff --git a/packages/next-server/server/api-utils.ts b/packages/next-server/server/api-utils.ts index 1712dee6f3fc..83fafe6e957d 100644 --- a/packages/next-server/server/api-utils.ts +++ b/packages/next-server/server/api-utils.ts @@ -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') @@ -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) } } @@ -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 */ @@ -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( + { 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 }) + }, + }) +} diff --git a/packages/next-server/server/load-components.ts b/packages/next-server/server/load-components.ts index 28c3dc784b29..143b939c9f10 100644 --- a/packages/next-server/server/load-components.ts +++ b/packages/next-server/server/load-components.ts @@ -14,6 +14,9 @@ export function interopDefault(mod: any) { export interface IPageConfig { amp?: boolean | 'hybrid' + api?: { + bodyParser?: boolean + } } export type LoadComponentsReturnType = { diff --git a/packages/next-server/server/next-server.ts b/packages/next-server/server/next-server.ts index 3c1fd13dac4d..71e41f4e6f85 100644 --- a/packages/next-server/server/next-server.ts +++ b/packages/next-server/server/next-server.ts @@ -23,15 +23,16 @@ 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' @@ -39,6 +40,7 @@ import { interopDefault, loadComponents, LoadComponentsReturnType, + IPageConfig, } from './load-components' import { renderToHTML } from './render' import { getPagePath } from './require' @@ -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) @@ -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) { diff --git a/packages/next/types/index.d.ts b/packages/next/types/index.d.ts index 68b69251c812..b29fdc8ead3c 100644 --- a/packages/next/types/index.d.ts +++ b/packages/next/types/index.d.ts @@ -48,6 +48,9 @@ export type NextPage

= { */ export type PageConfig = { amp?: boolean | 'hybrid' + api?: { + bodyParser?: boolean + } } export { NextPageContext, NextComponentType, NextApiResponse, NextApiRequest } diff --git a/test/integration/api-support/pages/api/no-parsing.js b/test/integration/api-support/pages/api/no-parsing.js new file mode 100644 index 000000000000..7dd787b1b852 --- /dev/null +++ b/test/integration/api-support/pages/api/no-parsing.js @@ -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())) + }) + } +} diff --git a/test/integration/api-support/pages/api/parsing.js b/test/integration/api-support/pages/api/parsing.js new file mode 100644 index 000000000000..65014ffcab67 --- /dev/null +++ b/test/integration/api-support/pages/api/parsing.js @@ -0,0 +1,11 @@ +export const config = { + api: { + bodyParser: true + } +} + +export default (req, res) => { + if (req.body) { + res.status(200).json({ message: 'Parsed body' }) + } +} diff --git a/test/integration/api-support/test/index.test.js b/test/integration/api-support/test/index.test.js index 639d3256df32..8e4c8e61a494 100644 --- a/test/integration/api-support/test/index.test.js +++ b/test/integration/api-support/test/index.test.js @@ -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()