Skip to content

Commit

Permalink
refactor: Improve Compatibility with standard Response object
Browse files Browse the repository at this point in the history
  • Loading branch information
usualoma committed Nov 21, 2023
1 parent 41ee775 commit 9133851
Show file tree
Hide file tree
Showing 3 changed files with 94 additions and 32 deletions.
6 changes: 3 additions & 3 deletions src/listener.ts
Original file line number Diff line number Diff line change
Expand Up @@ -33,13 +33,13 @@ const responseViaCache = (
res: Response,
outgoing: ServerResponse | Http2ServerResponse
): undefined | Promise<undefined> => {
const [body, header] = (res as any).__cache
const [status, body, header] = (res as any).__cache
if (typeof body === 'string') {
header['content-length'] ||= '' + Buffer.byteLength(body)
outgoing.writeHead((res as Response).status, header)
outgoing.writeHead(status, header)
outgoing.end(body)
} else {
outgoing.writeHead((res as Response).status, header)
outgoing.writeHead(status, header)
return writeFromReadableStream(body, outgoing)?.catch(
(e) => handleResponseError(e, outgoing) as undefined
)
Expand Down
62 changes: 33 additions & 29 deletions src/response.ts
Original file line number Diff line number Diff line change
@@ -1,58 +1,62 @@
// Define lightweight pseudo Response object and replace global.Response with it.
// Define lightweight pseudo Response class and replace global.Response with it.

import type { OutgoingHttpHeaders } from 'node:http'
import { buildOutgoingHttpHeaders } from './utils'

const globalResponse = global.Response
const responsePrototype: Record<string, any> = {
export const globalResponse = global.Response
class Response {
getResponseCache() {
delete this.__cache
return (this.responseCache ||= new globalResponse(this.__body, this.__init))
},
delete (this as any).__cache
return ((this as any).responseCache ||= new globalResponse(
(this as any).__body,
(this as any).__init
))
}

constructor(body: BodyInit | null, init?: ResponseInit) {
;(this as any).__body = body
;(this as any).__init = init

if (typeof body === 'string' || body instanceof ReadableStream) {
let headers = (init?.headers || { 'content-type': 'text/plain;charset=UTF-8' }) as
| Record<string, string>
| Headers
| OutgoingHttpHeaders
if (headers instanceof Headers) {
headers = buildOutgoingHttpHeaders(headers)
}

;(this as any).__cache = [init?.status || 200, body, headers]
}
}
}
;[
'body',
'bodyUsed',
'headers',
'ok',
'redirected',
'status',
'statusText',
'trailers',
'type',
'url',
].forEach((k) => {
Object.defineProperty(responsePrototype, k, {
Object.defineProperty(Response.prototype, k, {
get() {
return this.getResponseCache()[k]
},
})
})
;['arrayBuffer', 'blob', 'clone', 'error', 'formData', 'json', 'redirect', 'text'].forEach((k) => {
Object.defineProperty(responsePrototype, k, {
;['arrayBuffer', 'blob', 'clone', 'formData', 'json', 'text'].forEach((k) => {
Object.defineProperty(Response.prototype, k, {
value: function () {
return this.getResponseCache()[k]()
},
})
})

function newResponse(this: Response, body: BodyInit | null, init?: ResponseInit) {
;(this as any).status = init?.status || 200
;(this as any).__body = body
;(this as any).__init = init

if (typeof body === 'string' || body instanceof ReadableStream) {
let headers = (init?.headers || { 'content-type': 'text/plain;charset=UTF-8' }) as
| Record<string, string>
| Headers
| OutgoingHttpHeaders
if (headers instanceof Headers) {
headers = buildOutgoingHttpHeaders(headers)
}

;(this as any).__cache = [body, headers]
}
}
newResponse.prototype = responsePrototype
Object.setPrototypeOf(Response, globalResponse)
Object.setPrototypeOf(Response.prototype, globalResponse.prototype)
Object.defineProperty(global, 'Response', {
value: newResponse,
value: Response,
})
58 changes: 58 additions & 0 deletions test/response.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,58 @@
import { createServer, type Server } from 'node:http'
import { AddressInfo } from 'node:net'
import { globalResponse } from '../src/response'

class NextResponse extends Response {}

describe('Response', () => {
let server: Server
let port: number
beforeAll(
async () =>
new Promise<void>((resolve) => {
server = createServer((_, res) => {
res.writeHead(200, {
'Content-Type': 'application/json charset=UTF-8',
})
res.end(JSON.stringify({ status: 'ok' }))
})
.listen(0)
.on('listening', () => {
port = (server.address() as AddressInfo).port
resolve()
})
})
)

afterAll(() => {
server.close()
})

it('Compatibility with standard Response object', async () => {
// response name not changed
expect(Response.name).toEqual('Response')

// response prototype chain not changed
expect(new Response() instanceof globalResponse).toBe(true)

// `fetch()` and `Response` are not changed
const fetchRes = await fetch(`http://localhost:${port}`)
expect(new Response() instanceof fetchRes.constructor).toBe(true)
const resJson = await fetchRes.json()
expect(fetchRes.headers.get('content-type')).toEqual('application/json charset=UTF-8')
expect(resJson).toEqual({ status: 'ok' })

// can only use new operator
expect(() => {
;(Response as any)()
}).toThrow()

// support Response static method
expect(Response.error).toEqual(expect.any(Function))
expect((Response as any).json).toEqual(expect.any(Function))
expect(Response.redirect).toEqual(expect.any(Function))

// support other class to extends from Response
expect(NextResponse.prototype).toBeInstanceOf(Response)
})
})

0 comments on commit 9133851

Please sign in to comment.