diff --git a/__tests__/application/respond.js b/__tests__/application/respond.js index ccc57e269..db1af4543 100644 --- a/__tests__/application/respond.js +++ b/__tests__/application/respond.js @@ -570,6 +570,93 @@ describe('app.respond', () => { }) }) + describe('when .body is a Blob', () => { + it('should respond', async () => { + const app = new Koa() + + app.use(ctx => { + ctx.body = new Blob(['Hello']) + }) + + const expectedBlob = new Blob(['Hello']) + + const server = app.listen() + + const res = await request(server) + .get('/') + .expect(200) + + assert.deepStrictEqual(res.body, Buffer.from(await expectedBlob.arrayBuffer())) + }) + + it('should keep Blob headers', async () => { + const app = new Koa() + + app.use(ctx => { + ctx.body = new Blob(['hello world']) + }) + + const server = app.listen() + + return request(server) + .head('/') + .expect(200) + .expect('content-type', 'application/octet-stream') + .expect('content-length', '11') + }) + }) + + describe('when .body is a ReadableStream', () => { + it('should respond', async () => { + const app = new Koa() + + app.use(async ctx => { + ctx.body = new ReadableStream() + }) + + const server = app.listen() + + return request(server) + .head('/') + .expect(200) + .expect('content-type', 'application/octet-stream') + }) + }) + + describe('when .body is a Response', () => { + it('should keep Response headers', () => { + const app = new Koa() + + app.use(ctx => { + ctx.body = new Response(null, { status: 201, statusText: 'OK', headers: { 'Content-Type': 'text/plain' } }) + }) + + const server = app.listen() + + return request(server) + .head('/') + .expect(201) + .expect('content-type', 'text/plain') + .expect('content-length', '2') + }) + + it('should default to octet-stream', () => { + const app = new Koa() + + app.use(ctx => { + ctx.body = new Response(null, { status: 200, statusText: 'OK' }) + }) + + const server = app.listen() + + return request(server) + .head('/') + .expect(200) + .expect('content-type', 'application/octet-stream') + .expect('content-length', '2') + }) + }) + describe('when .body is a Stream', () => { it('should respond', async () => { const app = new Koa() diff --git a/__tests__/response/body.js b/__tests__/response/body.js index 5af050786..41c97a6db 100644 --- a/__tests__/response/body.js +++ b/__tests__/response/body.js @@ -141,4 +141,48 @@ describe('res.body=', () => { assert.strictEqual('application/json; charset=utf-8', res.header['content-type']) }) }) + + describe('when a ReadableStream is given', () => { + it('should default to an octet stream', () => { + const res = response() + res.body = new ReadableStream() + assert.strictEqual('application/octet-stream', res.header['content-type']) + }) + }) + + describe('when a Blob is given', () => { + it('should default to an octet stream', () => { + const res = response() + res.body = new Blob([new Uint8Array([1, 2, 3])], { type: 'application/octet-stream' }) + assert.strictEqual('application/octet-stream', res.header['content-type']) + }) + + it('should set length', () => { + const res = response() + res.body = new Blob([new Uint8Array([1, 2, 3])], { type: 'application/octet-stream' }) + assert.strictEqual('3', res.header['content-length']) + }) + }) + + describe('when a response is given', () => { + it('should set the status', () => { + const res = response() + res.body = new Response(null, { status: 201 }) + assert.strictEqual(201, res.status) + }) + + it('should set headers', () => { + const res = response() + res.body = new Response(null, { status: 200, headers: { 'x-fizz': 'buzz', 'x-foo': 'bar' } }) + assert.strictEqual('buzz', res.header['x-fizz']) + assert.strictEqual('bar', res.header['x-foo']) + }) + + it('should redirect', () => { + const res = response() + res.body = Response.redirect('https://www.example.com/', 301) + assert.strictEqual(301, res.status) + assert.strictEqual('https://www.example.com/', res.header.location) + }) + }) }) diff --git a/lib/application.js b/lib/application.js index ec7524dac..0ba1c8907 100644 --- a/lib/application.js +++ b/lib/application.js @@ -300,9 +300,13 @@ function respond (ctx) { } // responses + if (Buffer.isBuffer(body)) return res.end(body) if (typeof body === 'string') return res.end(body) if (body instanceof Stream) return body.pipe(res) + if (body instanceof Blob) return Stream.Readable.from(body.stream()).pipe(res) + if (body instanceof ReadableStream) return Stream.Readable.from(body).pipe(res) + if (body instanceof Response) return Stream.Readable.from(body?.body).pipe(res) // body: json body = JSON.stringify(body) diff --git a/lib/response.js b/lib/response.js index e6601569c..3dcd6e65e 100644 --- a/lib/response.js +++ b/lib/response.js @@ -126,15 +126,15 @@ module.exports = { /** * Set response body. * - * @param {String|Buffer|Object|Stream} val + * @param {String|Buffer|Object|Stream|ReadableStream|Blob|Response} val * @api public */ set body (val) { const original = this._body this._body = val - // no content + if (val == null) { if (!statuses.empty[this.status]) { if (this.type === 'application/json') { @@ -183,6 +183,34 @@ module.exports = { return } + // ReadableStream + if (val instanceof ReadableStream) { + if (setType) this.type = 'bin' + return + } + + // blob + if (val instanceof Blob) { + if (setType) this.type = 'bin' + this.length = val.size + return + } + + // Response + if (val instanceof Response) { + this.status = val.status + if (setType) this.type = 'bin' + const headers = val.headers + for (const key of headers.keys()) { + this.set(key, headers.get(key)) + } + + if (val.redirected) { + this.redirect(val.url) + } + return + } + // json this.remove('Content-Length') this.type = 'json'