Skip to content

fix: throw koa http errors from ctx.assert#1977

Open
puneetdixit200 wants to merge 2 commits into
koajs:masterfrom
puneetdixit200:fix-assert-http-error-instance
Open

fix: throw koa http errors from ctx.assert#1977
puneetdixit200 wants to merge 2 commits into
koajs:masterfrom
puneetdixit200:fix-assert-http-error-instance

Conversation

@puneetdixit200

@puneetdixit200 puneetdixit200 commented Jun 4, 2026

Copy link
Copy Markdown

Fixes #1925.

ctx.assert delegates to http-assert, which currently throws errors created by http-assert's nested http-errors@1.x dependency. Since Koa exports HttpError from its own http-errors@2.x dependency, errors thrown by ctx.assert() do not satisfy err instanceof Koa.HttpError.

This keeps http-assert's assertion behavior and helper methods, but converts thrown assertion errors through Koa's http-errors dependency so direct assertions and helper assertions return Koa HttpError instances.

Verification:

  • Red before fix: node --test __tests__/context/assert.test.js failed on err instanceof Koa.HttpError for ctx.assert(false, 404, ...).
  • Green after fix: node --test __tests__/context/assert.test.js
  • npm run build
  • npm run lint
  • git diff --check
  • npm test (442 tests, 0 failures)

Summary by Sourcery

Ensure context assertions throw Koa HttpError instances instead of raw http-assert errors.

Enhancements:

  • Wrap ctx.assert and its helper methods to rethrow assertion failures as Koa HttpError instances while preserving status, message, and properties.

Tests:

  • Add tests verifying that ctx.assert and its helper methods throw Koa HttpError instances with the expected status, message, and exposure.

Summary by CodeRabbit

  • Tests

    • Extended test coverage for context assertions to verify thrown errors' types, status codes, messages, and exposure flags, and to ensure non-HTTP assertion errors are rethrown unchanged.
  • Bug Fixes

    • Improved assertion handling in the runtime context so assertion failures are converted to consistent HTTP-style errors while preserving original error details and stack traces.

@coderabbitai

coderabbitai Bot commented Jun 4, 2026

Copy link
Copy Markdown

Review Change Stack

No actionable comments were generated in the recent review. 🎉

ℹ️ Recent review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro Plus

Run ID: 665725bc-4e78-4e26-9582-39e17c351f9c

📥 Commits

Reviewing files that changed from the base of the PR and between 6789dd0 and 48ac662.

📒 Files selected for processing (2)
  • __tests__/context/assert.test.js
  • lib/context.js
🚧 Files skipped from review as they are similar to previous changes (1)
  • lib/context.js

📝 Walkthrough

Walkthrough

This PR fixes ctx.assert() to throw Koa.HttpError instances as documented. The implementation wraps http-assert calls through a new rethrowKoaHttpError helper that intercepts errors with numeric status codes and converts them into http-errors instances while preserving original error properties. The wrapped assert function and its method variants replace the direct httpAssert reference on the context prototype. Tests verify that both direct and method-based assertions throw the correct error type with expected status, message, and expose fields.

sequenceDiagram
  participant Context
  participant rethrowKoaHttpError
  participant httpAssert
  participant createHttpError
  Context->>rethrowKoaHttpError: Context.assert(...args)
  rethrowKoaHttpError->>httpAssert: call underlying httpAssert method
  httpAssert-->>rethrowKoaHttpError: throws Error(status, message)
  rethrowKoaHttpError->>createHttpError: createHttpError(status, message)
  createHttpError-->>rethrowKoaHttpError: Koa.HttpError instance
  rethrowKoaHttpError-->>Context: throw Koa.HttpError (preserve props & stack)
Loading
🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 0.00% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (4 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title 'fix: throw koa http errors from ctx.assert' accurately reflects the main change: ensuring ctx.assert throws Koa's HttpError instances instead of nested http-errors versions.
Linked Issues check ✅ Passed The PR directly addresses issue #1925 by wrapping ctx.assert to convert assertion failures into Koa's HttpError instances, ensuring err instanceof Koa.HttpError returns true as required.
Out of Scope Changes check ✅ Passed All changes are scoped to fixing ctx.assert error handling: test file adds assertion error verification, context.js introduces wrapper to convert assertion errors to Koa HttpError instances.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.


Comment @coderabbitai help to get the list of available commands and usage tips.

@sourcery-ai

sourcery-ai Bot commented Jun 4, 2026

Copy link
Copy Markdown

Reviewer's Guide

Wraps ctx.assert and its helper methods so that assertion failures thrown by http-assert are re-thrown as Koa’s own HttpError instances, and adds tests verifying that both direct assertions and helper assertions now produce Koa HttpError objects.

Sequence diagram for ctx.assert rethrowing Koa HttpError

sequenceDiagram
  participant App
  participant Context
  participant rethrowKoaHttpError
  participant httpAssert
  participant createError

  App->>Context: assert(false, 404, message)
  Context->>rethrowKoaHttpError: rethrowKoaHttpError(httpAssert, args)
  rethrowKoaHttpError->>httpAssert: httpAssert(false, 404, message)
  httpAssert-->>rethrowKoaHttpError: throw err (from http-errors@1.x)
  alt err has numeric status
    rethrowKoaHttpError->>createError: createError(err.status, err.message, err)
    createError-->>App: throw HttpError (from http-errors@2.x)
  else other error
    rethrowKoaHttpError-->>App: throw err
  end
Loading

File-Level Changes

Change Details Files
Normalize assertion errors from ctx.assert to use Koa’s HttpError implementation instead of http-assert’s nested http-errors errors.
  • Introduce a rethrowKoaHttpError helper that intercepts errors from http-assert calls and, when they expose a numeric status, re-creates them via Koa’s createError while preserving properties.
  • Create a local assert wrapper that delegates to httpAssert through rethrowKoaHttpError and mirror all httpAssert helper methods to also go through this wrapper.
  • Update the exported context prototype to use the new assert wrapper in place of the raw httpAssert function.
lib/context.js
Verify that ctx.assert and its helper methods throw Koa HttpError instances with the expected shape.
  • Import the Koa module into the ctx.assert test suite so instanceof checks can be made against Koa.HttpError.
  • Add a test confirming ctx.assert(false, 404, 'custom message') throws an error that is an instance of Koa.HttpError with correct status, message, and expose fields.
  • Add a test confirming ctx.assert.equal(...) throws a Koa.HttpError instance with the expected status, message, and expose behavior.
__tests__/context/assert.test.js

Assessment against linked issues

Issue Objective Addressed Explanation
#1925 Ensure that ctx.assert() throws errors that are instances of Koa's exported HttpError class so that instanceof HttpError checks succeed.
#1925 Ensure that ctx.assert.* helper methods (e.g., ctx.assert.equal) also throw Koa HttpError instances with correct status, message, and expose properties.

Possibly linked issues

  • #[fix] ctx.assert() throws specific error classes: The PR ensures ctx.assert and helpers rethrow errors as Koa HttpError instances, fixing instanceof HttpError checks.

Tips and commands

Interacting with Sourcery

  • Trigger a new review: Comment @sourcery-ai review on the pull request.
  • Continue discussions: Reply directly to Sourcery's review comments.
  • Generate a GitHub issue from a review comment: Ask Sourcery to create an
    issue from a review comment by replying to it. You can also reply to a
    review comment with @sourcery-ai issue to create an issue from it.
  • Generate a pull request title: Write @sourcery-ai anywhere in the pull
    request title to generate a title at any time. You can also comment
    @sourcery-ai title on the pull request to (re-)generate the title at any time.
  • Generate a pull request summary: Write @sourcery-ai summary anywhere in
    the pull request body to generate a PR summary at any time exactly where you
    want it. You can also comment @sourcery-ai summary on the pull request to
    (re-)generate the summary at any time.
  • Generate reviewer's guide: Comment @sourcery-ai guide on the pull
    request to (re-)generate the reviewer's guide at any time.
  • Resolve all Sourcery comments: Comment @sourcery-ai resolve on the
    pull request to resolve all Sourcery comments. Useful if you've already
    addressed all the comments and don't want to see them anymore.
  • Dismiss all Sourcery reviews: Comment @sourcery-ai dismiss on the pull
    request to dismiss all existing Sourcery reviews. Especially useful if you
    want to start fresh with a new review - don't forget to comment
    @sourcery-ai review to trigger a new review!

Customizing Your Experience

Access your dashboard to:

  • Enable or disable review features such as the Sourcery-generated pull request
    summary, the reviewer's guide, and others.
  • Change the review language.
  • Add, remove or edit custom review instructions.
  • Adjust other review settings.

Getting Help

@codecov

codecov Bot commented Jun 4, 2026

Copy link
Copy Markdown

Codecov Report

✅ All modified and coverable lines are covered by tests.
✅ Project coverage is 99.90%. Comparing base (480a4f0) to head (48ac662).

Additional details and impacted files
@@           Coverage Diff           @@
##           master    #1977   +/-   ##
=======================================
  Coverage   99.90%   99.90%           
=======================================
  Files           9        9           
  Lines        2109     2130   +21     
=======================================
+ Hits         2107     2128   +21     
  Misses          2        2           

☔ View full report in Codecov by Harness.
📢 Have feedback on the report? Share it here.

🚀 New features to boost your workflow:
  • ❄️ Test Analytics: Detect flaky tests, report on failures, and find test suite problems.
  • 📦 JS Bundle Analysis: Save yourself from yourself by tracking and limiting bundle sizes in JS merges.

@sourcery-ai sourcery-ai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Hey - I've found 3 issues, and left some high level feedback:

  • The rethrowKoaHttpError helper currently wraps any thrown error that happens to have a numeric status field; consider narrowing this check (e.g., by also checking for a known marker property or constructor) so that arbitrary user errors with a status field are not silently converted into Koa.HttpError instances.
  • When re-wrapping the assertion error, the original stack trace is lost because stack is non-enumerable and not copied by Object.assign; if preserving debuggability is important, consider explicitly copying err.stack onto the new HttpError instance.
  • The assertion helper methods are re-exposed via arrow functions, which change metadata such as name and length compared to the original http-assert functions; if any downstream code relies on these, you may want to forward them in a way that preserves their observable shape (e.g., simple wrapper functions rather than arrow methods over Object.keys).
Prompt for AI Agents
Please address the comments from this code review:

## Overall Comments
- The `rethrowKoaHttpError` helper currently wraps any thrown error that happens to have a numeric `status` field; consider narrowing this check (e.g., by also checking for a known marker property or constructor) so that arbitrary user errors with a `status` field are not silently converted into `Koa.HttpError` instances.
- When re-wrapping the assertion error, the original stack trace is lost because `stack` is non-enumerable and not copied by `Object.assign`; if preserving debuggability is important, consider explicitly copying `err.stack` onto the new `HttpError` instance.
- The assertion helper methods are re-exposed via arrow functions, which change metadata such as `name` and `length` compared to the original `http-assert` functions; if any downstream code relies on these, you may want to forward them in a way that preserves their observable shape (e.g., simple wrapper functions rather than arrow methods over `Object.keys`).

## Individual Comments

### Comment 1
<location path="lib/context.js" line_range="16-25" />
<code_context>
+    return fn(...args)
+  } catch (err) {
+    if (err && typeof err.status === 'number') {
+      throw createError(err.status, err.message, Object.assign({}, err))
+    }
+    throw err
</code_context>
<issue_to_address>
**suggestion:** Rewrapping errors like this may drop important non-enumerable properties such as the original stack.

`Object.assign({}, err)` only copies enumerable own properties, so non-enumerable fields like `stack` are lost, which makes debugging harder.

If you’re just normalizing errors with a `status`, consider either:

- Passing the original error as props and adjusting only what you need:
  ```js
  const props = err instanceof Error ? err : { ...err }
  throw createError(err.status, err.message, props)
  ```

- Preserving existing HTTP/createError instances and only wrapping non-HTTP errors:
  ```js
  if (err && typeof err.status === 'number') {
    if (err.expose || err instanceof Error) throw err
    throw createError(err.status, err.message, { ...err })
  }
  ```

This keeps the original error details while still normalizing behavior.

```suggestion
function rethrowKoaHttpError (fn, args) {
  try {
    return fn(...args)
  } catch (err) {
    if (err && typeof err.status === 'number') {
      // Preserve existing HTTP/createError-style errors so we don't lose
      // important non-enumerable properties like stack, expose, etc.
      if (err.expose || err instanceof Error) {
        throw err
      }

      // For non-HTTP-ish errors with a status, wrap them while copying
      // enumerable properties for compatibility.
      const props = err instanceof Error ? err : Object.assign({}, err)
      throw createError(err.status, err.message, props)
    }
    throw err
  }
}
```
</issue_to_address>

### Comment 2
<location path="__tests__/context/assert.test.js" line_range="25-34" />
<code_context>
     assert(assertionRan)
   })
+
+  it('should throw Koa HttpError instances', () => {
+    const ctx = context()
+
+    assert.throws(() => {
+      ctx.assert(false, 404, 'custom message')
+    }, err => {
+      assert.strictEqual(err instanceof Koa.HttpError, true)
+      assert.strictEqual(err.status, 404)
+      assert.strictEqual(err.message, 'custom message')
+      assert.strictEqual(err.expose, true)
+      return true
+    })
+  })
</code_context>
<issue_to_address>
**suggestion (testing):** Add a test for `ctx.assert(false)` without an explicit status to cover the default 500 error case.

The new test only exercises `ctx.assert(false, 404, ...)`. Please add a sibling test for `ctx.assert(false)` (and/or `ctx.assert(false, 'message')`) that checks the thrown `Koa.HttpError` has the default 500 status and correct `expose` flag, so the non-explicit-status path is also covered for regressions.
</issue_to_address>

### Comment 3
<location path="__tests__/context/assert.test.js" line_range="39-51" />
<code_context>
+    })
+  })
+
+  it('should throw Koa HttpError instances from assertion helpers', () => {
+    const ctx = context()
+
+    assert.throws(() => {
+      ctx.assert.equal('actual', 'expected', 400, 'custom message')
+    }, err => {
+      assert.strictEqual(err instanceof Koa.HttpError, true)
+      assert.strictEqual(err.status, 400)
+      assert.strictEqual(err.message, 'custom message')
+      assert.strictEqual(err.expose, true)
+      return true
+    })
+  })
</code_context>
<issue_to_address>
**suggestion (testing):** Consider adding a test for an additional assertion helper or the default-status path for helpers.

This currently exercises only `ctx.assert.equal` with an explicit 400 status. Since all `http-assert` helpers are wrapped, add a test that either uses a different helper (e.g. `ctx.assert.ok` / `ctx.assert.notEqual`) or omits the status argument, then assert the thrown error is still a `Koa.HttpError`. That will show the wrapper behaves correctly across helpers and argument shapes.

```suggestion
  it('should throw Koa HttpError instances from assertion helpers', () => {
    const ctx = context()

    assert.throws(() => {
      ctx.assert.equal('actual', 'expected', 400, 'custom message')
    }, err => {
      assert.strictEqual(err instanceof Koa.HttpError, true)
      assert.strictEqual(err.status, 400)
      assert.strictEqual(err.message, 'custom message')
      assert.strictEqual(err.expose, true)
      return true
    })
  })

  it('should throw Koa HttpError instances from assertion helpers without explicit status', () => {
    const ctx = context()

    assert.throws(() => {
      ctx.assert.ok(false, 'custom message without status')
    }, err => {
      assert.strictEqual(err instanceof Koa.HttpError, true)
      assert.strictEqual(err.status, 500)
      assert.strictEqual(err.message, 'custom message without status')
      assert.strictEqual(err.expose, false)
      return true
    })
  })
```
</issue_to_address>

Sourcery is free for open source - if you like our reviews please consider sharing them ✨
Help me be more useful! Please click 👍 or 👎 on each comment and I'll use the feedback to improve your reviews.

Comment thread lib/context.js
Comment on lines +25 to +34
it('should throw Koa HttpError instances', () => {
const ctx = context()

assert.throws(() => {
ctx.assert(false, 404, 'custom message')
}, err => {
assert.strictEqual(err instanceof Koa.HttpError, true)
assert.strictEqual(err.status, 404)
assert.strictEqual(err.message, 'custom message')
assert.strictEqual(err.expose, true)

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

suggestion (testing): Add a test for ctx.assert(false) without an explicit status to cover the default 500 error case.

The new test only exercises ctx.assert(false, 404, ...). Please add a sibling test for ctx.assert(false) (and/or ctx.assert(false, 'message')) that checks the thrown Koa.HttpError has the default 500 status and correct expose flag, so the non-explicit-status path is also covered for regressions.

Comment on lines +39 to +51
it('should throw Koa HttpError instances from assertion helpers', () => {
const ctx = context()

assert.throws(() => {
ctx.assert.equal('actual', 'expected', 400, 'custom message')
}, err => {
assert.strictEqual(err instanceof Koa.HttpError, true)
assert.strictEqual(err.status, 400)
assert.strictEqual(err.message, 'custom message')
assert.strictEqual(err.expose, true)
return true
})
})

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

suggestion (testing): Consider adding a test for an additional assertion helper or the default-status path for helpers.

This currently exercises only ctx.assert.equal with an explicit 400 status. Since all http-assert helpers are wrapped, add a test that either uses a different helper (e.g. ctx.assert.ok / ctx.assert.notEqual) or omits the status argument, then assert the thrown error is still a Koa.HttpError. That will show the wrapper behaves correctly across helpers and argument shapes.

Suggested change
it('should throw Koa HttpError instances from assertion helpers', () => {
const ctx = context()
assert.throws(() => {
ctx.assert.equal('actual', 'expected', 400, 'custom message')
}, err => {
assert.strictEqual(err instanceof Koa.HttpError, true)
assert.strictEqual(err.status, 400)
assert.strictEqual(err.message, 'custom message')
assert.strictEqual(err.expose, true)
return true
})
})
it('should throw Koa HttpError instances from assertion helpers', () => {
const ctx = context()
assert.throws(() => {
ctx.assert.equal('actual', 'expected', 400, 'custom message')
}, err => {
assert.strictEqual(err instanceof Koa.HttpError, true)
assert.strictEqual(err.status, 400)
assert.strictEqual(err.message, 'custom message')
assert.strictEqual(err.expose, true)
return true
})
})
it('should throw Koa HttpError instances from assertion helpers without explicit status', () => {
const ctx = context()
assert.throws(() => {
ctx.assert.ok(false, 'custom message without status')
}, err => {
assert.strictEqual(err instanceof Koa.HttpError, true)
assert.strictEqual(err.status, 500)
assert.strictEqual(err.message, 'custom message without status')
assert.strictEqual(err.expose, false)
return true
})
})

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 1

🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@lib/context.js`:
- Around line 20-21: When recreating HTTP errors in rethrowKoaHttpError (the
block calling createError with Object.assign({}, err)), include the original
error.stack so the original stack trace is preserved; update the code that
builds the options for createError to copy err's enumerable properties and
explicitly set stack: err.stack (or otherwise ensure the created error's stack
is the original err.stack) while keeping status and message intact.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro Plus

Run ID: 8ccfb998-7a0d-4d7f-8893-4e36220eaafa

📥 Commits

Reviewing files that changed from the base of the PR and between 480a4f0 and 6789dd0.

📒 Files selected for processing (2)
  • __tests__/context/assert.test.js
  • lib/context.js

Comment thread lib/context.js Outdated
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

[fix] ctx.assert() throws specific error classes

1 participant