Skip to main content

Lifecycle Events

Keq provides an event mechanism that allows you to listen to various stages of the request lifecycle for logging, performance monitoring, debugging, and more.

Basic Usage

Use the .on() method to listen to events:

import { request } from 'keq'

await request
  .get('/cats')
  .on('fetch:before', ({ context }) => {
    console.log('About to send request:', context.request.url)
  })
  .on('fetch:after', ({ context }) => {
    console.log('Request completed:', context.response?.status)
  })

Instance-Level Event Listening

In addition to listening on individual requests, you can register event listeners on a KeqRequest instance for global event handling:

import { request } from 'keq'

// Listen for errors on all requests
request.on('error', ({ context }) => {
  console.error('Request failed:', context.request.__url__.href)
})

// Listen for timeouts on all requests
request.on('timeout', ({ context }) => {
  console.warn('Request timed out:', context.request.__url__.href)
})

// All subsequent requests will trigger the listeners above
await request.get('/cats')
await request.get('/dogs')
Events vs Middleware

The event system is designed for observation: the context in callbacks is read-only, and callbacks do not block request execution. If you need to modify requests or responses, use middleware.

Built-in Events

fetch:before

Triggered before the fetch request is sent.

Event parameters:

ParameterTypeDescription
contextKeqExecutionContextRequest execution context
import { request } from 'keq'

await request
  .get('/cats')
  .on('fetch:before', ({ context }) => {
    console.log('Request URL:', context.request.url)
    console.log('Request method:', context.request.method)
  })

fetch:after

Triggered after the fetch request completes.

Event parameters:

ParameterTypeDescription
contextKeqExecutionContextRequest execution context
import { request } from 'keq'

await request
  .get('/cats')
  .on('fetch:after', ({ context }) => {
    console.log('Response status:', context.response?.status)
  })

middleware:before

Triggered before middleware execution begins.

Event parameters:

ParameterTypeDescription
contextKeqExecutionContextRequest execution context
import { request } from 'keq'

await request
  .get('/cats')
  .on('middleware:before', ({ context }) => {
    console.log('About to execute middleware')
  })

middleware:after

Triggered after middleware execution completes.

Event parameters:

ParameterTypeDescription
contextKeqExecutionContextRequest execution context
import { request } from 'keq'

await request
  .get('/cats')
  .on('middleware:after', ({ context }) => {
    console.log('Middleware execution completed')
  })

retry

Triggered when a request is retried.

Event parameters:

ParameterTypeDescription
contextKeqSharedContextRequest shared context
import { request } from 'keq'

await request
  .get('/cats')
  .retry(3, 1000)
  .on('retry', ({ context }) => {
    console.log('Retrying request:', context.request.url)
  })

timeout

Triggered when a request times out.

Event parameters:

ParameterTypeDescription
contextKeqExecutionContextRequest execution context
import { request, TimeoutException } from 'keq'

try {
  await request
    .get('/cats')
    .timeout(3000)
    .on('timeout', ({ context }) => {
      console.log('Request timed out:', context.request.url)
    })
} catch (err) {
  if (err instanceof TimeoutException) {
    console.error('Request has timed out')
  }
}

abort

Triggered when a request is aborted.

Event parameters:

ParameterTypeDescription
contextKeqSharedContextRequest shared context
reasonanyAbort reason
import { request, AbortException, KeqMiddleware } from 'keq'

function autoAbort(): KeqMiddleware {
  return async (context, next) => {
    setTimeout(() => {
      context.request.abort(new AbortException('Manually aborted request'))
    }, 3000)
    await next()
  }
}

try {
  await request
    .get('/cats')
    .use(autoAbort())
    .on('abort', ({ context, reason }) => {
      if (reason instanceof AbortException) {
        console.log('Request aborted:', reason.message)
      } else {
        console.log('Request aborted:', reason)
      }
    })
} catch (err) {
  if (err instanceof AbortException) {
    console.error('Request was aborted:', err.message)
  }
}

error

Triggered when an error occurs during the request.

Event parameters:

ParameterTypeDescription
contextKeqSharedContextRequest shared context
import { request } from 'keq'

try {
  await request
    .get('/cats')
    .on('error', ({ context }) => {
      console.error('Request error occurred:', context.request.url)
    })
} catch (err) {
  console.error('Caught error:', err)
}

Custom Events

You can define and trigger custom events for more flexible feature extensions.

Defining Custom Event Types

import { KeqContext } from 'keq'

declare module 'keq' {
  interface KeqEvents {
    'custom:event': { context: KeqContext; data: string }
  }
}

Triggering Custom Events

import { request, KeqMiddleware } from 'keq'

function emitCustomEvent(): KeqMiddleware {
  return async (context, next) => {
    // Trigger custom event before request
    context.emitter.emit('custom:event', {
      context,
      data: 'custom data'
    })

    await next()
  }
}

await request
  .get('/cats')
  .use(emitCustomEvent())
  .on('custom:event', ({ context, data }) => {
    console.log('Custom event triggered:', data)
  })

Cleaning Up Event Listeners

import { request, KeqMiddleware } from 'keq'

function eventListenerMiddleware(): KeqMiddleware {
  return async (context, next) => {
    function onCustomEvent({ context, data }) {
      console.log('Received custom event:', data)
    }

    // Listen to event
    context.emitter.on('custom:event', onCustomEvent)

    await next()

    // Manually clean up event listener (optional — automatically removed after request ends)
    context.emitter.off('custom:event', onCustomEvent)
  }
}
tip

Event listeners are automatically cleaned up after the request ends, so you typically don't need to manually call .off(). However, in some scenarios (like needing to stop listening early), you can clean up manually.

Examples

Performance Monitoring

import { request, KeqMiddleware } from 'keq'

function metricsMiddleware(): KeqMiddleware {
  return async (context, next) => {
    context.emitter.on('fetch:before', ({ context }) => {
      context.data.metrics = { fetchStartAt: Date.now() }
    })

    context.emitter.on('fetch:after', ({ context }) => {
      const startTime = context.data.metrics?.fetchStartAt
      if (!startTime) return

      const duration = Date.now() - startTime
      console.log(`Request duration: ${duration}ms`)

      // Report performance data
      if (duration > 3000) {
        console.warn('Request took too long:', context.request.url)
      }

      // Report metrics to monitoring system
      reportMetrics({
        url: context.request.url,
        method: context.request.method,
        duration,
        status: context.response?.status
      })
    })

    await next()
  }
}

// Apply performance monitoring middleware globally
request.use(metricsMiddleware())

await request.get('/cats')

Request Logging

import { request, KeqMiddleware } from 'keq'

function requestLogger(): KeqMiddleware {
  return async (context, next) => {
    context.emitter.on('fetch:before', ({ context }) => {
      console.log(`[${new Date().toISOString()}] ${context.request.method} ${context.request.url}`)
    })

    context.emitter.on('fetch:after', ({ context }) => {
      const status = context.response?.status || 'N/A'
      console.log(`[${new Date().toISOString()}] ${context.request.method} ${context.request.url} - ${status}`)
    })

    await next()
  }
}

request.use(requestLogger())

await request.get('/cats')