Skip to content

Commit 661a123

Browse files
committed
refactor: Cut out Request and Response object of listener.ts as request.ts and response.ts.
1 parent 2cef3d9 commit 661a123

3 files changed

Lines changed: 116 additions & 105 deletions

File tree

src/listener.ts

Lines changed: 6 additions & 105 deletions
Original file line numberDiff line numberDiff line change
@@ -1,117 +1,14 @@
11
import type { IncomingMessage, ServerResponse, OutgoingHttpHeaders } from 'node:http'
22
import type { Http2ServerRequest, Http2ServerResponse } from 'node:http2'
3-
import { Readable } from 'node:stream'
43
import type { FetchCallback } from './types'
54
import './globals'
5+
import './response'
6+
import { requestPrototype } from './request'
67
import { writeFromReadableStream } from './utils'
78

89
const regBuffer = /^no$/i
910
const regContentType = /^(application\/json\b|text\/(?!event-stream\b))/i
1011

11-
const globalResponse = global.Response
12-
const responsePrototype: Record<string, any> = {
13-
getResponseCache() {
14-
delete this.__cache
15-
return (this.responseCache ||= new globalResponse(this.__body, this.__init))
16-
},
17-
}
18-
;[
19-
'body',
20-
'bodyUsed',
21-
'headers',
22-
'ok',
23-
'redirected',
24-
'statusText',
25-
'trailers',
26-
'type',
27-
'url',
28-
].forEach((k) => {
29-
Object.defineProperty(responsePrototype, k, {
30-
get() {
31-
return this.getResponseCache()[k]
32-
},
33-
})
34-
})
35-
;['arrayBuffer', 'blob', 'clone', 'error', 'formData', 'json', 'redirect', 'text'].forEach((k) => {
36-
Object.defineProperty(responsePrototype, k, {
37-
value: function () {
38-
return this.getResponseCache()[k]()
39-
},
40-
})
41-
})
42-
43-
function newResponse(this: Response, body: BodyInit | null, init?: ResponseInit) {
44-
;(this as any).status = init?.status || 200
45-
;(this as any).__body = body
46-
;(this as any).__init = init
47-
if (typeof body === 'string' || body instanceof ReadableStream) {
48-
;(this as any).__cache = [body, (init?.headers || {}) as Record<string, string>]
49-
}
50-
}
51-
newResponse.prototype = responsePrototype
52-
Object.defineProperty(global, 'Response', {
53-
value: newResponse,
54-
})
55-
56-
function newRequestFromIncoming(
57-
method: string,
58-
url: string,
59-
incoming: IncomingMessage | Http2ServerRequest
60-
): Request {
61-
const headerRecord: [string, string][] = []
62-
const len = incoming.rawHeaders.length
63-
for (let i = 0; i < len; i += 2) {
64-
headerRecord.push([incoming.rawHeaders[i], incoming.rawHeaders[i + 1]])
65-
}
66-
67-
const init = {
68-
method: method,
69-
headers: headerRecord,
70-
} as RequestInit
71-
72-
if (!(method === 'GET' || method === 'HEAD')) {
73-
// lazy-consume request body
74-
init.body = Readable.toWeb(incoming) as ReadableStream<Uint8Array>
75-
// node 18 fetch needs half duplex mode when request body is stream
76-
;(init as any).duplex = 'half'
77-
}
78-
79-
return new Request(url, init)
80-
}
81-
82-
const requestPrototype: Record<string, any> = {
83-
getRequestCache() {
84-
return (this.requestCache ||= newRequestFromIncoming(this.method, this.url, this.incoming))
85-
},
86-
}
87-
;[
88-
'body',
89-
'bodyUsed',
90-
'cache',
91-
'credentials',
92-
'destination',
93-
'headers',
94-
'integrity',
95-
'mode',
96-
'redirect',
97-
'referrer',
98-
'referrerPolicy',
99-
'signal',
100-
].forEach((k) => {
101-
Object.defineProperty(requestPrototype, k, {
102-
get() {
103-
return this.getRequestCache()[k]
104-
},
105-
})
106-
})
107-
;['arrayBuffer', 'blob', 'clone', 'formData', 'json', 'text'].forEach((k) => {
108-
Object.defineProperty(requestPrototype, k, {
109-
value: function () {
110-
return this.getRequestCache()[k]()
111-
},
112-
})
113-
})
114-
11512
const handleFetchError = (e: unknown): Response =>
11613
new Response(null, {
11714
status:
@@ -218,10 +115,14 @@ export const getRequestListener = (fetchCallback: FetchCallback) => {
218115
outgoing: ServerResponse | Http2ServerResponse
219116
) => {
220117
let res
118+
119+
// `fetchCallback()` requests a Request object, but global.Request is expensive to generate,
120+
// so generate a pseudo Request object with only the minimum required information.
221121
const req = Object.create(requestPrototype)
222122
req.method = incoming.method || 'GET'
223123
req.url = `http://${incoming.headers.host}${incoming.url}`
224124
req.incoming = incoming
125+
225126
try {
226127
res = fetchCallback(req) as Response | Promise<Response>
227128
if ('__cache' in res) {

src/request.ts

Lines changed: 64 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,64 @@
1+
// Define prototype for lightweight pseudo Request object
2+
3+
import { Readable } from 'node:stream'
4+
import type { IncomingMessage } from 'node:http'
5+
import type { Http2ServerRequest } from 'node:http2'
6+
7+
const newRequestFromIncoming = (
8+
method: string,
9+
url: string,
10+
incoming: IncomingMessage | Http2ServerRequest
11+
): Request => {
12+
const headerRecord: [string, string][] = []
13+
const len = incoming.rawHeaders.length
14+
for (let i = 0; i < len; i += 2) {
15+
headerRecord.push([incoming.rawHeaders[i], incoming.rawHeaders[i + 1]])
16+
}
17+
18+
const init = {
19+
method: method,
20+
headers: headerRecord,
21+
} as RequestInit
22+
23+
if (!(method === 'GET' || method === 'HEAD')) {
24+
// lazy-consume request body
25+
init.body = Readable.toWeb(incoming) as ReadableStream<Uint8Array>
26+
// node 18 fetch needs half duplex mode when request body is stream
27+
;(init as any).duplex = 'half'
28+
}
29+
30+
return new Request(url, init)
31+
}
32+
33+
export const requestPrototype: Record<string, any> = {
34+
getRequestCache() {
35+
return (this.requestCache ||= newRequestFromIncoming(this.method, this.url, this.incoming))
36+
},
37+
}
38+
;[
39+
'body',
40+
'bodyUsed',
41+
'cache',
42+
'credentials',
43+
'destination',
44+
'headers',
45+
'integrity',
46+
'mode',
47+
'redirect',
48+
'referrer',
49+
'referrerPolicy',
50+
'signal',
51+
].forEach((k) => {
52+
Object.defineProperty(requestPrototype, k, {
53+
get() {
54+
return this.getRequestCache()[k]
55+
},
56+
})
57+
})
58+
;['arrayBuffer', 'blob', 'clone', 'formData', 'json', 'text'].forEach((k) => {
59+
Object.defineProperty(requestPrototype, k, {
60+
value: function () {
61+
return this.getRequestCache()[k]()
62+
},
63+
})
64+
})

src/response.ts

Lines changed: 46 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,46 @@
1+
// Define lightweight pseudo Response object and replace global.Response with it.
2+
3+
const globalResponse = global.Response
4+
const responsePrototype: Record<string, any> = {
5+
getResponseCache() {
6+
delete this.__cache
7+
return (this.responseCache ||= new globalResponse(this.__body, this.__init))
8+
},
9+
}
10+
;[
11+
'body',
12+
'bodyUsed',
13+
'headers',
14+
'ok',
15+
'redirected',
16+
'statusText',
17+
'trailers',
18+
'type',
19+
'url',
20+
].forEach((k) => {
21+
Object.defineProperty(responsePrototype, k, {
22+
get() {
23+
return this.getResponseCache()[k]
24+
},
25+
})
26+
})
27+
;['arrayBuffer', 'blob', 'clone', 'error', 'formData', 'json', 'redirect', 'text'].forEach((k) => {
28+
Object.defineProperty(responsePrototype, k, {
29+
value: function () {
30+
return this.getResponseCache()[k]()
31+
},
32+
})
33+
})
34+
35+
function newResponse(this: Response, body: BodyInit | null, init?: ResponseInit) {
36+
;(this as any).status = init?.status || 200
37+
;(this as any).__body = body
38+
;(this as any).__init = init
39+
if (typeof body === 'string' || body instanceof ReadableStream) {
40+
;(this as any).__cache = [body, (init?.headers || {}) as Record<string, string>]
41+
}
42+
}
43+
newResponse.prototype = responsePrototype
44+
Object.defineProperty(global, 'Response', {
45+
value: newResponse,
46+
})

0 commit comments

Comments
 (0)