From a21ca973cd0115fa75fb4e624e6f3a9df4220ebf Mon Sep 17 00:00:00 2001 From: Jeff Wainwright <1074042+yowainwright@users.noreply.github.com> Date: Tue, 25 Aug 2026 20:20:18 -0700 Subject: [PATCH] fix: make HttpError instanceof recognize http-assert errors --- __tests__/context/assert.test.js | 88 ++++++++++++++++++++++++++++++++ lib/application.js | 15 ++++++ 2 files changed, 103 insertions(+) diff --git a/__tests__/context/assert.test.js b/__tests__/context/assert.test.js index b9dcb2dae..472518279 100644 --- a/__tests__/context/assert.test.js +++ b/__tests__/context/assert.test.js @@ -3,6 +3,33 @@ const { describe, it } = require('node:test') const context = require('../../test-helpers/context') const assert = require('node:assert/strict') +const httpAssert = require('http-assert') +const httpAssertCreateError = require(require.resolve('http-errors', { paths: [require.resolve('http-assert')] })) +const Koa = require('../..') + +const ASSERT_METHOD_FAILURES = [ + { method: 'fail', args: [400, 'custom message'] }, + { method: 'equal', args: [1, 2, 400, 'custom message'] }, + { method: 'notEqual', args: [1, '1', 400, 'custom message'] }, + { method: 'ok', args: [false, 400, 'custom message'] }, + { method: 'strictEqual', args: [1, '1', 400, 'custom message'] }, + { method: 'notStrictEqual', args: [1, 1, 400, 'custom message'] }, + { method: 'deepEqual', args: [{ ok: true }, { ok: false }, 400, 'custom message'] }, + { method: 'notDeepEqual', args: [{ ok: true }, { ok: true }, 400, 'custom message'] } +] + +function captureError (fn) { + let caught + + try { + fn() + } catch (err) { + caught = err + } + + assert(caught) + return caught +} describe('ctx.assert(value, status)', () => { it('should throw an error', () => { @@ -20,4 +47,65 @@ describe('ctx.assert(value, status)', () => { } assert(assertionRan) }) + + it('should throw an error that is instanceof Koa.HttpError', () => { + const ctx = context() + const err = captureError(() => ctx.assert(false, 404, 'custom message')) + + assert.strictEqual(err instanceof Koa.HttpError, true) + assert.strictEqual(Koa.isHttpError(err), true) + assert.strictEqual(err.status, 404) + assert.strictEqual(err.statusCode, 404) + assert.strictEqual(err.message, 'custom message') + assert.strictEqual(err.expose, true) + }) + + it('should preserve the original http-assert error', () => { + const ctx = context() + const err = captureError(() => ctx.assert(false, 401, 'custom message')) + + assert.strictEqual(ctx.assert, httpAssert) + assert.strictEqual(err instanceof httpAssertCreateError.HttpError, true) + assert.strictEqual(err.constructor, httpAssertCreateError.Unauthorized) + assert.strictEqual(err instanceof Koa.HttpError, true) + }) + + it('should preserve custom error options', () => { + const ctx = context() + const err = captureError(() => { + ctx.assert(false, 401, 'custom message', { + code: 'AUTH_REQUIRED', + headers: { + 'www-authenticate': 'Bearer' + } + }) + }) + + assert.strictEqual(err.code, 'AUTH_REQUIRED') + assert.deepStrictEqual(err.headers, { + 'www-authenticate': 'Bearer' + }) + }) + + it('should not throw when value is truthy', () => { + const ctx = context() + + ctx.assert(true, 404, 'custom message') + ctx.assert(1, 404) + ctx.assert('ok', 404) + }) +}) + +describe('ctx.assert named methods', () => { + ASSERT_METHOD_FAILURES.forEach(({ method, args }) => { + it(`ctx.assert.${method}() should throw an error that is instanceof Koa.HttpError`, () => { + const ctx = context() + const err = captureError(() => ctx.assert[method](...args)) + + assert.strictEqual(err instanceof Koa.HttpError, true) + assert.strictEqual(Koa.isHttpError(err), true) + assert.strictEqual(err.status, 400) + assert.strictEqual(err.message, 'custom message') + }) + }) }) diff --git a/lib/application.js b/lib/application.js index d455168d5..deb315811 100644 --- a/lib/application.js +++ b/lib/application.js @@ -22,6 +22,21 @@ const context = require('./context') const isStream = require('./is-stream.js') const only = require('./only.js') +function isHttpErrorLike (err) { + if (!err || typeof err !== 'object') return false + if (!(err instanceof Error)) return false + if (typeof err.expose !== 'boolean') return false + if (typeof err.statusCode !== 'number') return false + + const hasMatchingStatusCode = err.status === err.statusCode + return hasMatchingStatusCode +} + +Object.defineProperty(createHttpError.HttpError, Symbol.hasInstance, { + configurable: true, + value: isHttpErrorLike +}) + /** @typedef {typeof import ('./context') & { * app: Application * req: import('http').IncomingMessage