diff --git a/lib/handler/decorator-handler.js b/lib/handler/decorator-handler.js index 8a0d6c588a7..50fbb0cf892 100644 --- a/lib/handler/decorator-handler.js +++ b/lib/handler/decorator-handler.js @@ -1,6 +1,7 @@ 'use strict' const assert = require('node:assert') +const WrapHandler = require('./wrap-handler') /** * @deprecated @@ -9,63 +10,58 @@ module.exports = class DecoratorHandler { #handler #onCompleteCalled = false #onErrorCalled = false + #onResponseStartCalled = false constructor (handler) { if (typeof handler !== 'object' || handler === null) { throw new TypeError('handler must be an object') } - this.#handler = handler + this.#handler = WrapHandler.wrap(handler) } - onConnect (...args) { - return this.#handler.onConnect?.(...args) + onRequestStart (...args) { + this.#handler.onRequestStart?.(...args) } - onError (...args) { - this.#onErrorCalled = true - return this.#handler.onError?.(...args) - } - - onUpgrade (...args) { + onRequestUpgrade (...args) { assert(!this.#onCompleteCalled) assert(!this.#onErrorCalled) - return this.#handler.onUpgrade?.(...args) + return this.#handler.onRequestUpgrade?.(...args) } - onResponseStarted (...args) { + onResponseStart (...args) { assert(!this.#onCompleteCalled) assert(!this.#onErrorCalled) + assert(!this.#onResponseStartCalled) - return this.#handler.onResponseStarted?.(...args) - } - - onHeaders (...args) { - assert(!this.#onCompleteCalled) - assert(!this.#onErrorCalled) + this.#onResponseStartCalled = true - return this.#handler.onHeaders?.(...args) + return this.#handler.onResponseStart?.(...args) } - onData (...args) { + onResponseData (...args) { assert(!this.#onCompleteCalled) assert(!this.#onErrorCalled) - return this.#handler.onData?.(...args) + return this.#handler.onResponseData?.(...args) } - onComplete (...args) { + onResponseEnd (...args) { assert(!this.#onCompleteCalled) assert(!this.#onErrorCalled) this.#onCompleteCalled = true - return this.#handler.onComplete?.(...args) + return this.#handler.onResponseEnd?.(...args) } - onBodySent (...args) { - assert(!this.#onCompleteCalled) - assert(!this.#onErrorCalled) - - return this.#handler.onBodySent?.(...args) + onResponseError (...args) { + this.#onErrorCalled = true + return this.#handler.onResponseError?.(...args) } + + /** + * @deprecated + */ + onBodySent () {} } diff --git a/lib/interceptor/dns.js b/lib/interceptor/dns.js index 0cacf6cc959..c4fb7da19b5 100644 --- a/lib/interceptor/dns.js +++ b/lib/interceptor/dns.js @@ -2,7 +2,6 @@ const { isIP } = require('node:net') const { lookup } = require('node:dns') const DecoratorHandler = require('../handler/decorator-handler') -const WrapHandler = require('../handler/wrap-handler') const { InvalidArgumentError, InformationalError } = require('../core/errors') const maxInt = Math.pow(2, 31) - 1 @@ -223,39 +222,17 @@ class DNSDispatchHandler extends DecoratorHandler { #state = null #opts = null #dispatch = null - #handler = null #origin = null #controller = null constructor (state, { origin, handler, dispatch }, opts) { super(handler) this.#origin = origin - this.#handler = WrapHandler.wrap(handler) this.#opts = { ...opts } this.#state = state this.#dispatch = dispatch } - onRequestStart (controller, context) { - this.#handler.onRequestStart?.(controller, context) - } - - onRequestUpgrade (controller, statusCode, headers, socket) { - this.handler.onRequestUpgrade?.(controller, statusCode, headers, socket) - } - - onResponseStart (controller, statusCode, headers, statusMessage) { - this.#handler.onResponseStart?.(controller, statusCode, headers, statusMessage) - } - - onResponseData (controller, data) { - this.#handler.onResponseData?.(controller, data) - } - - onResponseEnd (controller, trailers) { - this.#handler.onResponseEnd?.(controller, trailers) - } - onResponseError (controller, err) { switch (err.code) { case 'ETIMEDOUT': @@ -264,7 +241,7 @@ class DNSDispatchHandler extends DecoratorHandler { // We delete the record and retry this.#state.runLookup(this.#origin, this.#opts, (err, newOrigin) => { if (err) { - this.#handler.onResponseError(controller, err) + super.onResponseError(controller, err) return } @@ -280,14 +257,14 @@ class DNSDispatchHandler extends DecoratorHandler { } // if dual-stack disabled, we error out - this.#handler.onResponseError(controller, err) + super.onResponseError(controller, err) break } case 'ENOTFOUND': this.#state.deleteRecord(this.#origin) // eslint-disable-next-line no-fallthrough default: - this.#handler.onResponseError(controller, err) + super.onResponseError(controller, err) break } } diff --git a/lib/interceptor/dump.js b/lib/interceptor/dump.js index 6e8a01032be..61c09d5c9ee 100644 --- a/lib/interceptor/dump.js +++ b/lib/interceptor/dump.js @@ -1,19 +1,17 @@ 'use strict' -const util = require('../core/util') const { InvalidArgumentError, RequestAbortedError } = require('../core/errors') const DecoratorHandler = require('../handler/decorator-handler') class DumpHandler extends DecoratorHandler { #maxSize = 1024 * 1024 - #abort = null #dumped = false - #aborted = false #size = 0 - #reason = null - #handler = null + #controller = null + aborted = false + reason = false - constructor ({ maxSize }, handler) { + constructor ({ maxSize, signal }, handler) { if (maxSize != null && (!Number.isFinite(maxSize) || maxSize < 1)) { throw new InvalidArgumentError('maxSize must be a number greater than 0') } @@ -21,23 +19,22 @@ class DumpHandler extends DecoratorHandler { super(handler) this.#maxSize = maxSize ?? this.#maxSize - this.#handler = handler + // this.#handler = handler } - onConnect (abort) { - this.#abort = abort - - this.#handler.onConnect(this.#customAbort.bind(this)) + #abort (reason) { + this.aborted = true + this.reason = reason } - #customAbort (reason) { - this.#aborted = true - this.#reason = reason + onRequestStart (controller, context) { + controller.abort = this.#abort.bind(this) + this.#controller = controller + + return super.onRequestStart(controller, context) } - // TODO: will require adjustment after new hooks are out - onHeaders (statusCode, rawHeaders, resume, statusMessage) { - const headers = util.parseHeaders(rawHeaders) + onResponseStart (controller, statusCode, headers, statusMessage) { const contentLength = headers['content-length'] if (contentLength != null && contentLength > this.#maxSize) { @@ -48,55 +45,50 @@ class DumpHandler extends DecoratorHandler { ) } - if (this.#aborted) { + if (this.aborted === true) { return true } - return this.#handler.onHeaders( - statusCode, - rawHeaders, - resume, - statusMessage - ) + return super.onResponseStart(controller, statusCode, headers, statusMessage) } - onError (err) { + onResponseError (controller, err) { if (this.#dumped) { return } - err = this.#reason ?? err + err = this.#controller.reason ?? err - this.#handler.onError(err) + super.onResponseError(controller, err) } - onData (chunk) { + onResponseData (controller, chunk) { this.#size = this.#size + chunk.length if (this.#size >= this.#maxSize) { this.#dumped = true - if (this.#aborted) { - this.#handler.onError(this.#reason) + if (this.aborted === true) { + super.onResponseError(controller, this.reason) } else { - this.#handler.onComplete([]) + super.onResponseEnd(controller, {}) } } return true } - onComplete (trailers) { + onResponseEnd (controller, trailers) { if (this.#dumped) { return } - if (this.#aborted) { - this.#handler.onError(this.reason) + if (this.#controller.aborted === true) { + super.onResponseError(controller, this.reason) return } - this.#handler.onComplete(trailers) + super.onResponseEnd(controller, trailers) } } @@ -107,13 +99,9 @@ function createDumpInterceptor ( ) { return dispatch => { return function Intercept (opts, handler) { - const { dumpMaxSize = defaultMaxSize } = - opts + const { dumpMaxSize = defaultMaxSize } = opts - const dumpHandler = new DumpHandler( - { maxSize: dumpMaxSize }, - handler - ) + const dumpHandler = new DumpHandler({ maxSize: dumpMaxSize, signal: opts.signal }, handler) return dispatch(opts, dumpHandler) } diff --git a/lib/interceptor/response-error.js b/lib/interceptor/response-error.js index 1df5c06165b..89ac1ee4ee0 100644 --- a/lib/interceptor/response-error.js +++ b/lib/interceptor/response-error.js @@ -1,43 +1,41 @@ 'use strict' -const { parseHeaders } = require('../core/util') +// const { parseHeaders } = require('../core/util') const DecoratorHandler = require('../handler/decorator-handler') const { ResponseError } = require('../core/errors') class ResponseErrorHandler extends DecoratorHandler { - #handler #statusCode #contentType #decoder #headers #body - constructor (opts, { handler }) { + constructor (_opts, { handler }) { super(handler) - this.#handler = handler } - onConnect (abort) { + #checkContentType (contentType) { + return this.#contentType.indexOf(contentType) === 0 + } + + onRequestStart (controller, context) { this.#statusCode = 0 this.#contentType = null this.#decoder = null this.#headers = null this.#body = '' - return this.#handler.onConnect(abort) - } - - #checkContentType (contentType) { - return this.#contentType.indexOf(contentType) === 0 + return super.onRequestStart(controller, context) } - onHeaders (statusCode, rawHeaders, resume, statusMessage, headers = parseHeaders(rawHeaders)) { + onResponseStart (controller, statusCode, headers, statusMessage) { this.#statusCode = statusCode this.#headers = headers this.#contentType = headers['content-type'] if (this.#statusCode < 400) { - return this.#handler.onHeaders(statusCode, rawHeaders, resume, statusMessage, headers) + return super.onResponseStart(controller, statusCode, headers, statusMessage) } if (this.#checkContentType('application/json') || this.#checkContentType('text/plain')) { @@ -45,15 +43,15 @@ class ResponseErrorHandler extends DecoratorHandler { } } - onData (chunk) { + onResponseData (controller, chunk) { if (this.#statusCode < 400) { - return this.#handler.onData(chunk) + return super.onResponseData(controller, chunk) } this.#body += this.#decoder?.decode(chunk, { stream: true }) ?? '' } - onComplete (rawTrailers) { + onResponseEnd (controller, trailers) { if (this.#statusCode >= 400) { this.#body += this.#decoder?.decode(undefined, { stream: false }) ?? '' @@ -77,14 +75,14 @@ class ResponseErrorHandler extends DecoratorHandler { Error.stackTraceLimit = stackTraceLimit } - this.#handler.onError(err) + super.onResponseError(controller, err) } else { - this.#handler.onComplete(rawTrailers) + super.onResponseEnd(controller, trailers) } } - onError (err) { - this.#handler.onError(err) + onResponseError (err) { + super.onResponseError(err) } } diff --git a/test/decorator-handler.js b/test/decorator-handler.js index a7777860e74..fc3e657f169 100644 --- a/test/decorator-handler.js +++ b/test/decorator-handler.js @@ -4,71 +4,402 @@ const { tspl } = require('@matteo.collina/tspl') const { describe, test } = require('node:test') const DecoratorHandler = require('../lib/handler/decorator-handler') -const methods = [ - 'onConnect', - 'onError', - 'onUpgrade', - 'onHeaders', - 'onResponseStarted', - 'onData', - 'onComplete', - 'onBodySent' -] - describe('DecoratorHandler', () => { - test('should throw if provided handler is not an object', (t) => { + test('should throw if provided handler is not an object', t => { t = tspl(t, { plan: 4 }) - t.throws(() => new DecoratorHandler(null), new TypeError('handler must be an object')) - t.throws(() => new DecoratorHandler('string'), new TypeError('handler must be an object')) + t.throws( + () => new DecoratorHandler(null), + new TypeError('handler must be an object') + ) + t.throws( + () => new DecoratorHandler('string'), + new TypeError('handler must be an object') + ) - t.throws(() => new DecoratorHandler(null), new TypeError('handler must be an object')) - t.throws(() => new DecoratorHandler('string'), new TypeError('handler must be an object')) + t.throws( + () => new DecoratorHandler(null), + new TypeError('handler must be an object') + ) + t.throws( + () => new DecoratorHandler('string'), + new TypeError('handler must be an object') + ) }) - test('should not expose the handler', (t) => { - t = tspl(t, { plan: 1 }) - const handler = {} - const decorator = new DecoratorHandler(handler) - t.strictEqual(Object.keys(decorator).length, 0) - }) + describe('wrap', () => { + const Handler = class { + #handler = null + constructor (handler) { + this.#handler = handler + } + + onConnect (abort, context) { + return this.#handler?.onConnect?.(abort, context) + } + + onHeaders (statusCode, rawHeaders, resume, statusMessage) { + return this.#handler?.onHeaders?.(statusCode, rawHeaders, resume, statusMessage) + } + + onUpgrade (statusCode, rawHeaders, socket) { + return this.#handler?.onUpgrade?.(statusCode, rawHeaders, socket) + } + + onData (data) { + return this.#handler?.onData?.(data) + } + + onComplete (trailers) { + return this.#handler?.onComplete?.(trailers) + } + + onError (err) { + return this.#handler?.onError?.(err) + } + } + const Controller = class { + #controller = null + constructor (controller) { + this.#controller = controller + } + + abort (reason) { + return this.#controller?.abort?.(reason) + } + + resume () { + return this.#controller?.resume?.() + } + + pause () { + return this.#controller?.pause?.() + } + } - methods.forEach((method) => { - test(`should have delegate ${method}-method`, (t) => { - t = tspl(t, { plan: 1 }) - const decorator = new DecoratorHandler({}) - t.equal(typeof decorator[method], 'function') + describe('#onConnect', () => { + test('should delegate onConnect-method', t => { + t = tspl(t, { plan: 2 }) + const handler = new Handler( + { + onConnect: (abort, ctx) => { + t.equal(typeof abort, 'function') + t.equal(typeof ctx, 'object') + } + }) + const decorator = new DecoratorHandler(handler) + decorator.onRequestStart(new Controller(), {}) + }) + + test('should not throw if onConnect-method is not defined in the handler', t => { + t = tspl(t, { plan: 1 }) + const decorator = new DecoratorHandler({}) + t.doesNotThrow(() => decorator.onRequestStart()) + }) }) - test(`should delegate ${method}-method`, (t) => { - t = tspl(t, { plan: 1 }) - const handler = { [method]: () => method } - const decorator = new DecoratorHandler(handler) - t.equal(decorator[method](), method) + describe('#onHeaders', () => { + test('should delegate onHeaders-method', t => { + t = tspl(t, { plan: 4 }) + const handler = new Handler( + { + onHeaders: (statusCode, headers, resume, statusMessage) => { + t.equal(statusCode, '200') + t.equal(`${headers[0].toString('utf-8')}: ${headers[1].toString('utf-8')}`, 'content-type: application/json') + t.equal(typeof resume, 'function') + t.equal(statusMessage, 'OK') + } + }) + const decorator = new DecoratorHandler(handler) + decorator.onResponseStart(new Controller(), 200, { + 'content-type': 'application/json' + }, 'OK') + }) + + test('should not throw if onHeaders-method is not defined in the handler', t => { + t = tspl(t, { plan: 1 }) + const decorator = new DecoratorHandler({}) + t.doesNotThrow(() => decorator.onResponseStart(new Controller(), 200, { + 'content-type': 'application/json' + })) + }) }) - test(`should delegate ${method}-method with arguments`, (t) => { - t = tspl(t, { plan: 1 }) - const handler = { [method]: (...args) => args } - const decorator = new DecoratorHandler(handler) - t.deepStrictEqual(decorator[method](1, 2, 3), [1, 2, 3]) + describe('#onUpgrade', () => { + test('should delegate onUpgrade-method', t => { + t = tspl(t, { plan: 3 }) + const handler = new Handler( + { + onUpgrade: (statusCode, headers, socket) => { + t.equal(statusCode, 301) + t.equal(`${headers[0].toString('utf-8')}: ${headers[1].toString('utf-8')}`, 'content-type: application/json') + t.equal(typeof socket, 'object') + } + }) + const decorator = new DecoratorHandler(handler) + decorator.onRequestUpgrade(new Controller(), 301, { + 'content-type': 'application/json' + }, {}) + }) + + test('should not throw if onUpgrade-method is not defined in the handler', t => { + t = tspl(t, { plan: 1 }) + const decorator = new DecoratorHandler({}) + t.doesNotThrow(() => decorator.onRequestUpgrade(new Controller(), 301, { + 'content-type': 'application/json' + })) + }) + }) + + describe('#onData', () => { + test('should delegate onData-method', t => { + t = tspl(t, { plan: 1 }) + const handler = new Handler( + { + onData: (chunk) => { + t.equal('chunk', chunk) + } + }) + const decorator = new DecoratorHandler(handler) + decorator.onResponseData(new Controller(), 'chunk') + }) + + test('should not throw if onData-method is not defined in the handler', t => { + t = tspl(t, { plan: 1 }) + const decorator = new DecoratorHandler({}) + t.doesNotThrow(() => decorator.onResponseData(new Controller(), 'chunk')) + }) + }) + + describe('#onComplete', () => { + test('should delegate onComplete-method', t => { + t = tspl(t, { plan: 1 }) + const handler = new Handler( + { + onComplete: (trailers) => { + t.equal(`${trailers[0].toString('utf-8')}: ${trailers[1].toString('utf-8')}`, 'x-trailer: trailer') + } + }) + const decorator = new DecoratorHandler(handler) + decorator.onResponseEnd(new Controller(), { 'x-trailer': 'trailer' }) + }) + + test('should not throw if onComplete-method is not defined in the handler', t => { + t = tspl(t, { plan: 1 }) + const decorator = new DecoratorHandler({}) + t.doesNotThrow(() => decorator.onResponseEnd(new Controller(), { 'x-trailer': 'trailer' })) + }) + }) + + describe('#onError', () => { + test('should delegate onError-method', t => { + t = tspl(t, { plan: 1 }) + const handler = new Handler( + { + onError: (err) => { + t.equal(err.message, 'Oops!') + } + }) + const decorator = new DecoratorHandler(handler) + decorator.onResponseError(new Controller(), new Error('Oops!')) + }) + + test('should throw if onError-method is not defined in the handler', t => { + t = tspl(t, { plan: 1 }) + const decorator = new DecoratorHandler({}) + t.throws(() => decorator.onResponseError(new Controller(), new Error('Oops!'))) + }) }) + }) + + describe('no-wrap', () => { + const Handler = class { + #handler = null + constructor (handler) { + this.#handler = handler + } + + onRequestStart (controller, context) { + return this.#handler?.onRequestStart?.(controller, context) + } + + onRequestUpgrade (controller, statusCode, headers, socket) { + return this.#handler?.onRequestUpgrade?.(controller, statusCode, headers, socket) + } + + onResponseStart (controller, statusCode, headers, statusMessage) { + return this.#handler?.onResponseStart?.(controller, statusCode, headers, statusMessage) + } + + onResponseData (controller, data) { + return this.#handler?.onResponseData?.(controller, data) + } + + onResponseEnd (controller, trailers) { + return this.#handler?.onResponseEnd?.(controller, trailers) + } + + onResponseError (controller, err) { + return this.#handler?.onResponseError?.(controller, err) + } + } + const Controller = class { + #controller = null + constructor (controller) { + this.#controller = controller + } + + abort (reason) { + return this.#controller?.abort?.(reason) + } - test(`can be extended and should delegate ${method}-method`, (t) => { - t = tspl(t, { plan: 1 }) + resume () { + return this.#controller?.resume?.() + } - class ExtendedHandler extends DecoratorHandler { - [method] () { - return method - } + pause () { + return this.#controller?.pause?.() } - const decorator = new ExtendedHandler({}) - t.equal(decorator[method](), method) + } + + describe('#onRequestStart', () => { + test('should delegate onRequestStart-method', t => { + t = tspl(t, { plan: 2 }) + const handler = new Handler( + { + onRequestStart: (controller, ctx) => { + t.equal(controller.constructor, Controller) + t.equal(typeof ctx, 'object') + } + }) + const decorator = new DecoratorHandler(handler) + decorator.onRequestStart(new Controller(), {}) + }) + + test('should not throw if onRequestStart-method is not defined in the handler', t => { + t = tspl(t, { plan: 1 }) + const decorator = new DecoratorHandler({}) + t.doesNotThrow(() => decorator.onRequestStart()) + }) }) - test(`calling the method ${method}-method should not throw if the method is not defined in the handler`, (t) => { - t = tspl(t, { plan: 1 }) - const decorator = new DecoratorHandler({}) - t.doesNotThrow(() => decorator[method]()) + describe('#onRequestUpgrade', () => { + test('should delegate onRequestUpgrade-method', t => { + t = tspl(t, { plan: 4 }) + const handler = new Handler( + { + onRequestUpgrade: (controller, statusCode, headers, socket) => { + t.equal(controller.constructor, Controller) + t.equal(statusCode, 301) + t.equal(headers['content-type'], 'application/json') + t.equal(typeof socket, 'object') + } + }) + const decorator = new DecoratorHandler(handler) + decorator.onRequestUpgrade(new Controller(), 301, { + 'content-type': 'application/json' + }, {}) + }) + + test('should not throw if onRequestUpgrade-method is not defined in the handler', t => { + t = tspl(t, { plan: 1 }) + const decorator = new DecoratorHandler({}) + t.doesNotThrow(() => decorator.onRequestUpgrade(new Controller(), 301, { + 'content-type': 'application/json' + }, {})) + }) + }) + + describe('#onResponseStart', () => { + test('should delegate onResponseStart-method', t => { + t = tspl(t, { plan: 4 }) + const handler = new Handler( + { + onResponseStart: (controller, statusCode, headers, message) => { + t.equal(controller.constructor, Controller) + t.equal(statusCode, 200) + t.equal(headers['content-type'], 'application/json') + t.equal(message, 'OK') + } + }) + const decorator = new DecoratorHandler(handler) + decorator.onResponseStart(new Controller(), 200, { + 'content-type': 'application/json' + }, 'OK') + }) + + test('should not throw if onResponseStart-method is not defined in the handler', t => { + t = tspl(t, { plan: 1 }) + const decorator = new DecoratorHandler({}) + t.doesNotThrow(() => decorator.onResponseStart(new Controller(), 200, { + 'content-type': 'application/json' + }, 'OK')) + }) + }) + + describe('#onResponseData', () => { + test('should delegate onResponseData-method', t => { + t = tspl(t, { plan: 2 }) + const handler = new Handler( + { + onResponseData: (controller, chunk) => { + t.equal(controller.constructor, Controller) + t.equal('chunk', chunk) + } + }) + const decorator = new DecoratorHandler(handler) + decorator.onResponseData(new Controller(), 'chunk') + }) + + test('should not throw if onResponseData-method is not defined in the handler', t => { + t = tspl(t, { plan: 1 }) + const decorator = new DecoratorHandler({}) + t.doesNotThrow(() => decorator.onResponseData(new Controller(), 'chunk')) + }) + }) + + describe('#onResponseEnd', () => { + test('should delegate onResponseEnd-method', t => { + t = tspl(t, { plan: 2 }) + const handler = new Handler( + { + onResponseEnd: (controller, trailers) => { + t.equal(controller.constructor, Controller) + t.equal(trailers['x-trailer'], 'trailer') + } + }) + const decorator = new DecoratorHandler(handler) + decorator.onResponseEnd(new Controller(), { 'x-trailer': 'trailer' }) + }) + + test('should not throw if onResponseEnd-method is not defined in the handler', t => { + t = tspl(t, { plan: 1 }) + const decorator = new DecoratorHandler({}) + t.doesNotThrow(() => decorator.onResponseEnd(new Controller(), { 'x-trailer': 'trailer' })) + }) + }) + + describe('#onResponseError', () => { + test('should delegate onError-method', t => { + t = tspl(t, { plan: 2 }) + const handler = new Handler( + { + onResponseError: (controller, err) => { + t.equal(controller.constructor, Controller) + t.equal(err.message, 'Oops!') + } + }) + const decorator = new DecoratorHandler(handler) + decorator.onResponseError(new Controller(), new Error('Oops!')) + }) + + test('should throw if onError-method is not defined in the handler', t => { + t = tspl(t, { plan: 1 }) + const decorator = new DecoratorHandler({ + // To hin and not wrap the instance + onRequestStart: () => {} + }) + t.doesNotThrow(() => decorator.onResponseError(new Controller())) + }) }) }) })