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')