From 757e4ebc5146784ebd00664545c6d37231610a61 Mon Sep 17 00:00:00 2001 From: Matthieu Tabarin Date: Thu, 20 Jun 2024 05:26:12 +0100 Subject: [PATCH 1/3] deps: make "only" package inside (#1824) closes https://github.com/koajs/koa/issues/1802 --- lib/application.js | 2 +- lib/only.js | 11 +++++++++++ lib/request.js | 2 +- lib/response.js | 2 +- package-lock.json | 6 ------ package.json | 1 - 6 files changed, 14 insertions(+), 10 deletions(-) create mode 100644 lib/only.js diff --git a/lib/application.js b/lib/application.js index bbee69067..ec7524dac 100644 --- a/lib/application.js +++ b/lib/application.js @@ -16,7 +16,7 @@ const Emitter = require('events') const util = require('util') const Stream = require('stream') const http = require('http') -const only = require('only') +const only = require('./only.js') const { HttpError } = require('http-errors') /** @typedef {typeof import ('./context') & { diff --git a/lib/only.js b/lib/only.js new file mode 100644 index 000000000..0551a3354 --- /dev/null +++ b/lib/only.js @@ -0,0 +1,11 @@ +module.exports = (obj, keys) => { + obj = obj || {} + if (typeof keys === 'string') keys = keys.split(/ +/) + const ret = {} + for (let i = 0; i < keys.length; i++) { + const key = keys[i] + if (obj[key] == null) continue + ret[key] = obj[key] + } + return ret +} diff --git a/lib/request.js b/lib/request.js index 7a1a07c12..b756a7a06 100644 --- a/lib/request.js +++ b/lib/request.js @@ -13,7 +13,7 @@ const parse = require('parseurl') const qs = require('querystring') const typeis = require('type-is') const fresh = require('fresh') -const only = require('only') +const only = require('./only.js') const util = require('util') const IP = Symbol('context#ip') diff --git a/lib/response.js b/lib/response.js index cbe587142..e6601569c 100644 --- a/lib/response.js +++ b/lib/response.js @@ -14,7 +14,7 @@ const destroy = require('destroy') const assert = require('assert') const extname = require('path').extname const vary = require('vary') -const only = require('only') +const only = require('./only.js') const util = require('util') const encodeUrl = require('encodeurl') const Stream = require('stream') diff --git a/package-lock.json b/package-lock.json index e9a7c684e..676339fa7 100644 --- a/package-lock.json +++ b/package-lock.json @@ -24,7 +24,6 @@ "http-errors": "^1.6.3", "koa-compose": "^4.1.0", "on-finished": "^2.3.0", - "only": "~0.0.2", "parseurl": "^1.3.2", "statuses": "^1.5.0", "type-is": "^1.6.16", @@ -5380,11 +5379,6 @@ "url": "https://github.com/sponsors/sindresorhus" } }, - "node_modules/only": { - "version": "0.0.2", - "resolved": "https://registry.npmjs.org/only/-/only-0.0.2.tgz", - "integrity": "sha1-Kv3oTQPlC5qO3EROMGEKcCle37Q=" - }, "node_modules/optionator": { "version": "0.9.3", "resolved": "https://registry.npmjs.org/optionator/-/optionator-0.9.3.tgz", diff --git a/package.json b/package.json index a8e3378e4..820ca40cc 100644 --- a/package.json +++ b/package.json @@ -51,7 +51,6 @@ "http-errors": "^1.6.3", "koa-compose": "^4.1.0", "on-finished": "^2.3.0", - "only": "~0.0.2", "parseurl": "^1.3.2", "statuses": "^1.5.0", "type-is": "^1.6.16", From 8d5beb329d0851a9e2eb00ed08d3996ebde53994 Mon Sep 17 00:00:00 2001 From: Matthieu Tabarin Date: Fri, 28 Jun 2024 15:45:36 +0100 Subject: [PATCH 2/3] feat: add support web WHATWG (#1830) closes https://github.com/koajs/koa/issues/1777 --- __tests__/application/respond.js | 87 ++++++++++++++++++++++++++++++++ __tests__/response/body.js | 44 ++++++++++++++++ lib/application.js | 4 ++ lib/response.js | 32 +++++++++++- 4 files changed, 165 insertions(+), 2 deletions(-) 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' From cfdb77f34d6f5aa4110c5425e78565165d51c0a7 Mon Sep 17 00:00:00 2001 From: Tommy Dew Date: Fri, 28 Jun 2024 23:26:17 +0800 Subject: [PATCH 3/3] docs: Add writable getter to the response doc (#1832) Fixes #1680 --- docs/api/response.md | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/docs/api/response.md b/docs/api/response.md index e75b30152..1ac65cc8e 100644 --- a/docs/api/response.md +++ b/docs/api/response.md @@ -351,3 +351,7 @@ ctx.response.etag = crypto.createHash('md5').update(ctx.body).digest('hex'); ### response.flushHeaders() Flush any set headers, and begin the body. + +### response.writable + + A boolean that indicates whether the response is still writable