Skip to content

Commit c3eb8ea

Browse files
stafyniaksachapi0
andauthored
feat: add stream pipe response (#68)
Co-authored-by: Pooya Parsa <pyapar@gmail.com>
1 parent 11ef8e3 commit c3eb8ea

File tree

3 files changed

+59
-1
lines changed

3 files changed

+59
-1
lines changed

src/app.ts

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,7 @@ import type { IncomingMessage, ServerResponse } from './types/node'
33
import { lazyHandle, promisifyHandle } from './handle'
44
import type { Handle, LazyHandle, Middleware, PHandle } from './handle'
55
import { createError, sendError } from './error'
6-
import { send, MIMES } from './utils'
6+
import { send, sendStream, isStream, MIMES } from './utils'
77

88
export interface Layer {
99
route: string
@@ -114,6 +114,8 @@ export function createHandle (stack: Stack, options: AppOptions): PHandle {
114114
const type = typeof val
115115
if (type === 'string') {
116116
return send(res, val, MIMES.html)
117+
} else if (isStream(val)) {
118+
return sendStream(res, val)
117119
} else if (type === 'object' || type === 'boolean' || type === 'number' /* IS_JSON */) {
118120
if (val && val.buffer) {
119121
return send(res, val)

src/utils/response.ts

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
import type { ServerResponse } from 'http'
2+
import { createError } from '../error'
23
import { MIMES } from './consts'
34

45
const defer = typeof setImmediate !== 'undefined' ? setImmediate : (fn: Function) => fn()
@@ -41,3 +42,15 @@ export function appendHeader (res: ServerResponse, name: string, value: string):
4142

4243
res.setHeader(name, current.concat(value))
4344
}
45+
46+
export function isStream (data: any) {
47+
return typeof data === 'object' && typeof data.pipe === 'function' && typeof data.on === 'function'
48+
}
49+
50+
export function sendStream (res: ServerResponse, data: any) {
51+
return new Promise((resolve, reject) => {
52+
data.pipe(res)
53+
data.on('end', () => resolve(undefined))
54+
data.on('error', (error: Error) => reject(createError(error)))
55+
})
56+
}

test/app.test.ts

Lines changed: 43 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,4 @@
1+
import { Readable, Transform } from 'stream'
12
import supertest, { SuperTest, Test } from 'supertest'
23
import { describe, it, expect, beforeEach } from 'vitest'
34
import { createApp, App } from '../src'
@@ -18,6 +19,48 @@ describe('app', () => {
1819
expect(res.body).toEqual({ url: '/' })
1920
})
2021

22+
it('can return Buffer directly', async () => {
23+
app.use(() => Buffer.from('<h1>Hello world!</h1>', 'utf8'))
24+
const res = await request.get('/')
25+
26+
expect(res.text).toBe('<h1>Hello world!</h1>')
27+
})
28+
29+
it('can return Readable stream directly', async () => {
30+
app.use(() => {
31+
const readable = new Readable()
32+
readable.push(Buffer.from('<h1>Hello world!</h1>', 'utf8'))
33+
readable.push(null)
34+
return readable
35+
})
36+
const res = await request.get('/')
37+
38+
expect(res.text).toBe('<h1>Hello world!</h1>')
39+
expect(res.header['transfer-encoding']).toBe('chunked')
40+
})
41+
42+
it('can return Readable stream that may throw', async () => {
43+
app.use(() => {
44+
const readable = new Readable()
45+
const willThrow = new Transform({
46+
transform (
47+
_chunk,
48+
_encoding,
49+
callback
50+
) {
51+
setTimeout(() => callback(new Error('test')), 0)
52+
}
53+
})
54+
readable.push(Buffer.from('<h1>Hello world!</h1>', 'utf8'))
55+
readable.push(null)
56+
57+
return readable.pipe(willThrow)
58+
})
59+
const res = await request.get('/')
60+
61+
expect(res.status).toBe(500)
62+
})
63+
2164
it('can return HTML directly', async () => {
2265
app.use(() => '<h1>Hello world!</h1>')
2366
const res = await request.get('/')

0 commit comments

Comments
 (0)