@@ -6,11 +6,9 @@ import {
6
6
NowResponse ,
7
7
} from '@vercel/node' ;
8
8
import { IncomingMessage , ServerResponse , Server , RequestListener } from 'http' ;
9
- import { parse } from 'cookie' ;
10
- import { parse as parseContentType } from 'content-type' ;
11
- import { parse as parseQS } from 'querystring' ;
12
- import { URL } from 'url' ;
13
9
import micro , { buffer , send } from 'micro' ;
10
+ // @ts -expect-error
11
+ import cloneResponse from 'clone-response' ;
14
12
15
13
export class ApiError extends Error {
16
14
readonly statusCode : number ;
@@ -21,69 +19,98 @@ export class ApiError extends Error {
21
19
}
22
20
}
23
21
24
- function parseBody ( req : IncomingMessage , body : Buffer ) : NowRequestBody {
25
- if ( ! req . headers [ 'content-type' ] ) {
26
- return undefined ;
27
- }
28
-
29
- const { type } = parseContentType ( req . headers [ 'content-type' ] ) ;
22
+ function getBodyParser ( req : NowRequest , body : Buffer | string ) {
23
+ return function parseBody ( ) : NowRequestBody {
24
+ if ( ! req . headers [ 'content-type' ] ) {
25
+ return undefined ;
26
+ }
27
+ // eslint-disable-next-line @typescript-eslint/no-var-requires
28
+ const { parse : parseContentType } = require ( 'content-type' ) ;
29
+ const { type } = parseContentType ( req . headers [ 'content-type' ] ) ;
30
+
31
+ if ( type === 'application/json' ) {
32
+ try {
33
+ const str = body . toString ( ) ;
34
+ return str ? JSON . parse ( str ) : { } ;
35
+ } catch ( error ) {
36
+ throw new ApiError ( 400 , 'Invalid JSON' ) ;
37
+ }
38
+ }
30
39
31
- if ( type === 'application/json' ) {
32
- try {
33
- return JSON . parse ( body . toString ( ) ) ;
34
- } catch ( error ) {
35
- throw new ApiError ( 400 , 'Invalid JSON' ) ;
40
+ if ( type === 'application/octet-stream' ) {
41
+ return body ;
36
42
}
37
- }
38
43
39
- if ( type === 'application/octet-stream' ) {
40
- return body ;
41
- }
44
+ if ( type === 'application/x-www-form-urlencoded' ) {
45
+ // eslint-disable-next-line @typescript-eslint/no-var-requires
46
+ const { parse : parseQS } = require ( 'querystring' ) ;
47
+ // note: querystring.parse does not produce an iterable object
48
+ // https://nodejs.org/api/querystring.html#querystring_querystring_parse_str_sep_eq_options
49
+ return parseQS ( body . toString ( ) ) ;
50
+ }
42
51
43
- if ( type === 'application/x-www-form-urlencoded' ) {
44
- // note: querystring.parse does not produce an iterable object
45
- // https://nodejs.org/api/querystring.html#querystring_querystring_parse_str_sep_eq_options
46
- return parseQS ( body . toString ( ) ) ;
47
- }
52
+ if ( type === 'text/plain' ) {
53
+ return body . toString ( ) ;
54
+ }
48
55
49
- if ( type === 'text/plain' ) {
50
- return body . toString ( ) ;
51
- }
56
+ return undefined ;
57
+ } ;
58
+ }
52
59
53
- return undefined ;
60
+ function getQueryParser ( { url = '/' } : NowRequest ) {
61
+ return function parseQuery ( ) : NowRequestQuery {
62
+ // eslint-disable-next-line @typescript-eslint/no-var-requires
63
+ const { parse : parseURL } = require ( 'url' ) ;
64
+ return parseURL ( url , true ) . query ;
65
+ } ;
54
66
}
55
67
56
- function parseQuery ( { url = '/' } : IncomingMessage ) : NowRequestQuery {
57
- // we provide a placeholder base url because we only want searchParams
58
- const params = new URL ( url , 'https://n' ) . searchParams ;
68
+ function getCookieParser ( req : NowRequest ) {
69
+ return function parseCookie ( ) : NowRequestCookies {
70
+ const header : undefined | string | string [ ] = req . headers . cookie ;
71
+
72
+ if ( ! header ) {
73
+ return { } ;
74
+ }
75
+
76
+ // eslint-disable-next-line @typescript-eslint/no-var-requires
77
+ const { parse } = require ( 'cookie' ) ;
78
+ return parse ( Array . isArray ( header ) ? header . join ( ';' ) : header ) ;
79
+ } ;
80
+ }
59
81
60
- const query : { [ key : string ] : string | string [ ] } = { } ;
61
- params . forEach ( ( value , name ) => {
62
- query [ name ] = value ;
82
+ function setLazyProp < T > ( req : NowRequest , prop : string , getter : ( ) => T ) {
83
+ const opts = { configurable : true , enumerable : true } ;
84
+ const optsReset = { ...opts , writable : true } ;
85
+
86
+ Object . defineProperty ( req , prop , {
87
+ ...opts ,
88
+ get : ( ) => {
89
+ const value = getter ( ) ;
90
+ // we set the property on the object to avoid recalculating it
91
+ Object . defineProperty ( req , prop , { ...optsReset , value } ) ;
92
+ return value ;
93
+ } ,
94
+ set : value => {
95
+ Object . defineProperty ( req , prop , { ...optsReset , value } ) ;
96
+ } ,
63
97
} ) ;
64
- return query ;
65
98
}
66
99
67
- function parseCookie ( req : IncomingMessage ) : NowRequestCookies {
68
- const header : undefined | string | string [ ] = req . headers . cookie ;
69
- if ( ! header ) {
70
- return { } ;
100
+ export const enhanceRequest = async ( req : NowRequest ) : Promise < NowRequest > => {
101
+ // We clone the request, so that we can read the incoming stream but then
102
+ // still allow subsequent consumers to do the same
103
+ const reqClone = cloneResponse ( req ) ;
104
+ const newReq = cloneResponse ( req ) ;
105
+ const body = await buffer ( reqClone ) ;
106
+
107
+ setLazyProp < NowRequestCookies > ( newReq , 'cookies' , getCookieParser ( newReq ) ) ;
108
+ setLazyProp < NowRequestQuery > ( newReq , 'query' , getQueryParser ( newReq ) ) ;
109
+ if ( body != null ) {
110
+ setLazyProp < NowRequestBody > ( newReq , 'body' , getBodyParser ( newReq , body ) ) ;
71
111
}
72
- return parse ( Array . isArray ( header ) ? header . join ( ';' ) : header ) ;
73
- }
74
112
75
- export const enhanceRequest = async (
76
- req : IncomingMessage
77
- ) : Promise < NowRequest > => {
78
- const bufferOrString = await buffer ( req ) ;
79
- return Object . assign ( req , {
80
- body :
81
- typeof bufferOrString === 'string'
82
- ? bufferOrString
83
- : parseBody ( req , bufferOrString ) ,
84
- cookies : parseCookie ( req ) ,
85
- query : parseQuery ( req ) ,
86
- } ) ;
113
+ return newReq ;
87
114
} ;
88
115
89
116
export const enhanceResponse = ( res : ServerResponse ) : NowResponse => {
@@ -132,6 +159,7 @@ export const createServer = <C extends Config = DefaultConfig>(
132
159
return new Server ( route ) ;
133
160
} else {
134
161
return micro ( async ( req : IncomingMessage , res : ServerResponse ) => {
162
+ // @ts -expect-error
135
163
const nowReq = await enhanceRequest ( req ) ;
136
164
const nowRes = enhanceResponse ( res ) ;
137
165
return await route ( nowReq , nowRes ) ;
0 commit comments