From de186cbbf00f6f6870cffa93c5ba31df95d857ae Mon Sep 17 00:00:00 2001 From: Konstantin Vyatkin Date: Tue, 15 Oct 2019 17:18:46 -0400 Subject: [PATCH 1/6] add support for object mode streams --- lib/application.js | 31 ++++++++++++++++++++++++++++++- lib/response.js | 11 +++++++++-- test/response/body.js | 8 ++++++++ 3 files changed, 47 insertions(+), 3 deletions(-) diff --git a/lib/application.js b/lib/application.js index 46f43134e..534746e42 100644 --- a/lib/application.js +++ b/lib/application.js @@ -249,7 +249,36 @@ function respond(ctx) { // responses if (Buffer.isBuffer(body)) return res.end(body); if ('string' == typeof body) return res.end(body); - if (body instanceof Stream) return body.pipe(res); + if (body instanceof Stream) { + // check if it's an objectMode stream + // readableObjectMode is available since Node 12.3 + if (body.readableObjectMode || + (body._readableState && + body._readableState.objectMode)) { + let first = true; + body = body + .pipe(new Stream.Transform({ + writableObjectMode: true, + readableObjectMode: false, + transform(data, encoding, callback) { + const str = JSON.stringify(data); + if (first) { + this.push(`[${str}`); + first = false; + } else { + this.push(`,${str}`); + } + return callback(); + }, + flush(callback) { + if (!first) this.push(`]`); + this.push(null); + return callback(); + } + })); + } + return body.pipe(res); + } // body: json body = JSON.stringify(body); diff --git a/lib/response.js b/lib/response.js index 87ecc56a9..28e471bc0 100644 --- a/lib/response.js +++ b/lib/response.js @@ -166,14 +166,21 @@ module.exports = { } // stream - if ('function' == typeof val.pipe) { + if (val instanceof Stream) { onFinish(this.res, destroy.bind(null, val)); ensureErrorHandler(val, err => this.ctx.onerror(err)); // overwriting if (null != original && original != val) this.remove('Content-Length'); - if (setType) this.type = 'bin'; + if (setType) { + // check if it's an objectMode stream + // readableObjectMode is available since Node 12.3 + this.type = (val.readableObjectMode || + (val._readableState && + val._readableState.objectMode)) + ? 'json' : 'bin'; + } return; } diff --git a/test/response/body.js b/test/response/body.js index 1dfcac55e..aa617c8da 100644 --- a/test/response/body.js +++ b/test/response/body.js @@ -4,6 +4,7 @@ const response = require('../helpers/context').response; const assert = require('assert'); const fs = require('fs'); +const { PassThrough } = require('stream'); describe('res.body=', () => { describe('when Content-Type is set', () => { @@ -108,6 +109,13 @@ describe('res.body=', () => { res.body = fs.createReadStream('LICENSE'); assert.equal('application/octet-stream', res.header['content-type']); }); + + it('object mode stream', () => { + const res = response(); + const stream = new PassThrough({ objectMode: true }); + res.body = stream; + assert.equal('application/json; charset=utf-8', res.header['content-type']); + }); }); describe('when a buffer is given', () => { From 73644e918d515fcb4972b9d008cb17e7f35f8b43 Mon Sep 17 00:00:00 2001 From: Konstantin Vyatkin Date: Tue, 15 Oct 2019 17:26:22 -0400 Subject: [PATCH 2/6] add test --- test/application/respond.js | 22 ++++++++++++++++++++++ 1 file changed, 22 insertions(+) diff --git a/test/application/respond.js b/test/application/respond.js index d254ead6e..eea82ed91 100644 --- a/test/application/respond.js +++ b/test/application/respond.js @@ -6,6 +6,7 @@ const statuses = require('statuses'); const assert = require('assert'); const Koa = require('../..'); const fs = require('fs'); +const { PassThrough } = require('stream'); describe('app.respond', () => { describe('when ctx.respond === false', () => { @@ -618,6 +619,27 @@ describe('app.respond', () => { assert.deepEqual(res.body, pkg); }); + it('should serve object mode streams as JSON', () => { + const app = new Koa(); + + app.use(ctx => { + const stream = new PassThrough({ objectMode: true, autoDestroy: false }); + ctx.body = stream; + setImmediate(() => { + stream.write({ foo: 1 }); + stream.write({ boo: [true] }); + stream.end({ finish: true }); + }); + }); + + const server = app.listen(); + + return request(server) + .get('/') + .expect('Content-Type', 'application/json; charset=utf-8') + .expect('[{"foo":1},{"boo":[true]},{"finish":true}]'); + }); + it('should handle errors', done => { const app = new Koa(); From a2faabb0427d959dc2795dbeae819d522abf79c4 Mon Sep 17 00:00:00 2001 From: Konstantin Vyatkin Date: Tue, 15 Oct 2019 17:31:04 -0400 Subject: [PATCH 3/6] ensure content-length is not set for json stream --- test/application/respond.js | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/test/application/respond.js b/test/application/respond.js index eea82ed91..114bb09b2 100644 --- a/test/application/respond.js +++ b/test/application/respond.js @@ -619,7 +619,7 @@ describe('app.respond', () => { assert.deepEqual(res.body, pkg); }); - it('should serve object mode streams as JSON', () => { + it('should serve object mode streams as JSON', async() => { const app = new Koa(); app.use(ctx => { @@ -634,10 +634,11 @@ describe('app.respond', () => { const server = app.listen(); - return request(server) + const res = await request(server) .get('/') .expect('Content-Type', 'application/json; charset=utf-8') .expect('[{"foo":1},{"boo":[true]},{"finish":true}]'); + assert.strictEqual(res.headers.hasOwnProperty('content-length'), false); }); it('should handle errors', done => { From 85a79d39e7f908c5c66e6e5c10fb1a88db81424b Mon Sep 17 00:00:00 2001 From: Konstantin Vyatkin Date: Tue, 15 Oct 2019 17:43:36 -0400 Subject: [PATCH 4/6] empty object mode stream should result in empty array --- lib/application.js | 5 +++-- test/application/respond.js | 18 ++++++++++++++++++ 2 files changed, 21 insertions(+), 2 deletions(-) diff --git a/lib/application.js b/lib/application.js index 534746e42..f4024b564 100644 --- a/lib/application.js +++ b/lib/application.js @@ -256,6 +256,7 @@ function respond(ctx) { (body._readableState && body._readableState.objectMode)) { let first = true; + res.write('['); body = body .pipe(new Stream.Transform({ writableObjectMode: true, @@ -263,7 +264,7 @@ function respond(ctx) { transform(data, encoding, callback) { const str = JSON.stringify(data); if (first) { - this.push(`[${str}`); + this.push(`${str}`); first = false; } else { this.push(`,${str}`); @@ -271,7 +272,7 @@ function respond(ctx) { return callback(); }, flush(callback) { - if (!first) this.push(`]`); + this.push(`]`); this.push(null); return callback(); } diff --git a/test/application/respond.js b/test/application/respond.js index 114bb09b2..e2136d96a 100644 --- a/test/application/respond.js +++ b/test/application/respond.js @@ -641,6 +641,24 @@ describe('app.respond', () => { assert.strictEqual(res.headers.hasOwnProperty('content-length'), false); }); + it('empty object mode stream should result in empty array', async() => { + const app = new Koa(); + + app.use(ctx => { + const stream = new PassThrough({ objectMode: true, autoDestroy: false }); + ctx.body = stream; + setImmediate(() => stream.end()); + }); + + const server = app.listen(); + + const res = await request(server) + .get('/') + .expect('Content-Type', 'application/json; charset=utf-8') + .expect('[]'); + assert.strictEqual(res.headers.hasOwnProperty('content-length'), false); + }); + it('should handle errors', done => { const app = new Koa(); From 489f9e8f70c11c2ac3335ec0b2527996b7d2f52c Mon Sep 17 00:00:00 2001 From: Konstantin Vyatkin Date: Tue, 15 Oct 2019 18:37:13 -0400 Subject: [PATCH 5/6] more semantic --- lib/application.js | 11 ++++------- 1 file changed, 4 insertions(+), 7 deletions(-) diff --git a/lib/application.js b/lib/application.js index f4024b564..229e7e1bc 100644 --- a/lib/application.js +++ b/lib/application.js @@ -262,13 +262,10 @@ function respond(ctx) { writableObjectMode: true, readableObjectMode: false, transform(data, encoding, callback) { - const str = JSON.stringify(data); - if (first) { - this.push(`${str}`); - first = false; - } else { - this.push(`,${str}`); - } + if (!first) this.push(','); + else first = false; + + this.push(JSON.stringify(data)); return callback(); }, flush(callback) { From 2f25df86529f457b24234c7d23d5086285cb0902 Mon Sep 17 00:00:00 2001 From: Konstantin Vyatkin Date: Fri, 1 Nov 2019 00:53:53 -0400 Subject: [PATCH 6/6] fix lint --- lib/application.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/application.js b/lib/application.js index c826323ed..765c86771 100644 --- a/lib/application.js +++ b/lib/application.js @@ -273,7 +273,7 @@ function respond(ctx) { return callback(); }, flush(callback) { - this.push(`]`); + this.push(']'); this.push(null); return callback(); }