Back to Repositories

Testing Error Handling Middleware Implementation in Koa.js

This test suite validates the error handling middleware (onerror) functionality in Koa.js, focusing on how the framework processes and responds to various error scenarios during HTTP request handling. The tests ensure proper error status codes, header management, and error event emission.

Test Coverage Overview

The test suite provides comprehensive coverage of error handling scenarios in Koa.js:

  • HTTP status code handling and validation
  • Header management during error states
  • Custom error properties and exposure
  • Error handling after headers are sent
  • ENOENT and non-standard error processing

Implementation Analysis

The testing approach utilizes Jest’s describe/it blocks to organize test cases logically. Tests leverage supertest for HTTP request simulation and assertion patterns that verify both response properties and internal state management. The implementation demonstrates proper async/await patterns and error event handling.

Technical Details

Testing tools and configuration:

  • Node.js native test runner
  • Supertest for HTTP assertions
  • Custom context helpers
  • VM module for cross-context error testing
  • Assert module for validation

Best Practices Demonstrated

The test suite exemplifies several testing best practices:

  • Isolated test cases with clear scope
  • Comprehensive error scenario coverage
  • Proper async/await handling
  • Event emission verification
  • Header and status code validation

koajs/koa

__tests__/context/onerror.test.js

            
'use strict'

const { describe, it } = require('node:test')
const assert = require('assert')
const request = require('supertest')
const Koa = require('../..')
const context = require('../../test-helpers/context')

describe('ctx.onerror(err)', () => {
  it('should respond', () => {
    const app = new Koa()

    app.use((ctx, next) => {
      ctx.body = 'something else'

      ctx.throw(418, 'boom')
    })

    return request(app.callback())
      .get('/')
      .expect(418)
      .expect('Content-Type', 'text/plain; charset=utf-8')
      .expect('Content-Length', '4')
  })

  it('should unset all headers', async () => {
    const app = new Koa()

    app.use((ctx, next) => {
      ctx.set('Vary', 'Accept-Encoding')
      ctx.set('X-CSRF-Token', 'asdf')
      ctx.body = 'response'

      ctx.throw(418, 'boom')
    })

    const res = await request(app.callback())
      .get('/')
      .expect(418)
      .expect('Content-Type', 'text/plain; charset=utf-8')
      .expect('Content-Length', '4')

    assert.strictEqual(Object.prototype.hasOwnProperty.call(res.headers, 'vary'), false)
    assert.strictEqual(Object.prototype.hasOwnProperty.call(res.headers, 'x-csrf-token'), false)
  })

  it('should set headers specified in the error', async () => {
    const app = new Koa()

    app.use((ctx, next) => {
      ctx.set('Vary', 'Accept-Encoding')
      ctx.set('X-CSRF-Token', 'asdf')
      ctx.body = 'response'

      throw Object.assign(new Error('boom'), {
        status: 418,
        expose: true,
        headers: {
          'X-New-Header': 'Value'
        }
      })
    })

    const res = await request(app.callback())
      .get('/')
      .expect(418)
      .expect('Content-Type', 'text/plain; charset=utf-8')
      .expect('X-New-Header', 'Value')

    assert.strictEqual(Object.prototype.hasOwnProperty.call(res.headers, 'vary'), false)
    assert.strictEqual(Object.prototype.hasOwnProperty.call(res.headers, 'x-csrf-token'), false)
  })

  it('should ignore error after headerSent', (t, done) => {
    const app = new Koa()

    app.on('error', (err, { res }) => {
      assert.strictEqual(err.message, 'mock error')
      assert.strictEqual(err.headerSent, true)
      res.end()
      done()
    })

    app.use(async ctx => {
      ctx.status = 200
      ctx.set('X-Foo', 'Bar')
      ctx.flushHeaders()
      await Promise.reject(new Error('mock error'))
      ctx.body = 'response'
    })

    request(app.callback())
      .get('/')
      .expect('X-Foo', 'Bar')
      .expect(200, () => {})
  })

  it('should set status specified in the error using statusCode', () => {
    const app = new Koa()

    app.use((ctx, next) => {
      ctx.body = 'something else'
      const err = new Error('Not found')
      err.statusCode = 404
      throw err
    })

    return request(app.callback())
      .get('/')
      .expect(404)
      .expect('Content-Type', 'text/plain; charset=utf-8')
      .expect('Not Found')
  })

  describe('when invalid err.statusCode', () => {
    describe('not number', () => {
      it('should respond 500', () => {
        const app = new Koa()

        app.use((ctx, next) => {
          ctx.body = 'something else'
          const err = new Error('some error')
          err.statusCode = 'notnumber'
          throw err
        })

        return request(app.callback())
          .get('/')
          .expect(500)
          .expect('Content-Type', 'text/plain; charset=utf-8')
          .expect('Internal Server Error')
      })
    })
  })

  describe('when invalid err.status', () => {
    describe('not number', () => {
      it('should respond 500', () => {
        const app = new Koa()

        app.use((ctx, next) => {
          ctx.body = 'something else'
          const err = new Error('some error')
          err.status = 'notnumber'
          throw err
        })

        return request(app.callback())
          .get('/')
          .expect(500)
          .expect('Content-Type', 'text/plain; charset=utf-8')
          .expect('Internal Server Error')
      })
    })
    describe('when ENOENT error', () => {
      it('should respond 404', () => {
        const app = new Koa()

        app.use((ctx, next) => {
          ctx.body = 'something else'
          const err = new Error('test for ENOENT')
          err.code = 'ENOENT'
          throw err
        })

        return request(app.callback())
          .get('/')
          .expect(404)
          .expect('Content-Type', 'text/plain; charset=utf-8')
          .expect('Not Found')
      })
    })
    describe('not http status code', () => {
      it('should respond 500', () => {
        const app = new Koa()

        app.use((ctx, next) => {
          ctx.body = 'something else'
          const err = new Error('some error')
          err.status = 9999
          throw err
        })

        return request(app.callback())
          .get('/')
          .expect(500)
          .expect('Content-Type', 'text/plain; charset=utf-8')
          .expect('Internal Server Error')
      })
    })
  })

  describe('when error from another scope thrown', () => {
    it('should handle it like a normal error', async () => {
      const ExternError = require('vm').runInNewContext('Error')

      const app = new Koa()
      const error = Object.assign(new ExternError('boom'), {
        status: 418,
        expose: true
      })
      app.use((ctx, next) => {
        throw error
      })

      const gotRightErrorPromise = new Promise((resolve, reject) => {
        app.on('error', receivedError => {
          try {
            assert.strictEqual(receivedError, error)
            resolve()
          } catch (e) {
            reject(e)
          }
        })
      })

      await request(app.callback())
        .get('/')
        .expect(418)

      await gotRightErrorPromise
    })
  })

  describe('when non-error thrown', () => {
    it('should respond with non-error thrown message', () => {
      const app = new Koa()

      app.use((ctx, next) => {
        throw 'string error' // eslint-disable-line no-throw-literal
      })

      return request(app.callback())
        .get('/')
        .expect(500)
        .expect('Content-Type', 'text/plain; charset=utf-8')
        .expect('Internal Server Error')
    })

    it('should use res.getHeaderNames() accessor when available', () => {
      let removed = 0
      const ctx = context()

      ctx.app.emit = () => {}
      ctx.res = {
        getHeaderNames: () => ['content-type', 'content-length'],
        removeHeader: () => removed++,
        end: () => {},
        emit: () => {}
      }

      ctx.onerror(new Error('error'))

      assert.strictEqual(removed, 2)
    })

    it('should stringify error if it is an object', (t, done) => {
      const app = new Koa()

      app.on('error', err => {
        assert.strictEqual(err.message, 'non-error thrown: {"key":"value"}')
        done()
      })

      app.use(async ctx => {
        throw { key: 'value' } // eslint-disable-line no-throw-literal
      })

      request(app.callback())
        .get('/')
        .expect(500)
        .expect('Internal Server Error', () => {})
    })
  })
})