Handlers

This document covers all of the types and APIs pertaining to request handlers. You can learn more about handlers in the concepts document.

# Handler Properties

# bodyParsers

Added in 0.5.0.

A list of body parsers to call when context.body is accessed. Replaces the app-attached body parsers for the any request routed to the handler, including in middleware that executes before the handler.

Example use:

// handlers.js
module.exports = { myHandler }
const { body } = require('./boltzmann')
const yaml = require('yaml')

myHandler.route = 'POST /'
myHandler.bodyParsers = [
  body.json,
  yamlParser // handle yaml!
]
async function myHandler (context) {
  await context.body
}

function yamlParser (next) {
  return async request => {
    if (
      request.contentType.type === 'application' &&
      request.contentType.subtype === 'yaml'
    ) {
      const chunks = []
      for await (const chunk of request) {
        chunks.push(chunk)
      }

      return yaml.parse(String(Buffer.concat(chunks)))
  }
}

# decorators

Added in 0.0.0. Changelog
  • Changed in 0.1.2: Deprecated in favor of handler-attached middleware.

Deprecated. Use middleware instead.

Decorators were the initial version of handler-attached middleware. They were deprecated because they conflict with a language-level feature of the same name. Decorators are middleware with the arguments applied:

// handlers.js
module.exports = { decorated, middlewared }
const { middleware } = require('./boltzmann')

decorated.route = 'GET /decor'
decorated.decorators = [
  middleware.validate.body({
    type: 'object',
    properties: {
      'greeting': { type: 'string', minLength: 3 }
    },
    required: 'greeting'
  })
]
async function decorated (context) {
}

middlewared.route = 'GET /middle'
middlewared.middleware = [
  [middleware.validate.body, { // note that we provide args as an array
    type: 'object',            // here, like we do with APP_MIDDLEWARE.
    properties: {
      'greeting': { type: 'string', minLength: 3 }
    },
    required: 'greeting'
  }]
]
async function middlewared (context) {
}

# method

Added in 0.0.0.

A string or array of strings representing the HTTP verbs that route to this handler. In addition to the well-known HTTP verbs, Boltzmann treats * as "any HTTP verb is allowed." Overrides the method from the .route = 'VERB /route' shorthand.

Example use:

// handlers.js
module.exports = { multimethod, any, shorthand }

multimethod.route = '/foo'
multimethod.method = ['GET', 'POST']
async function multimethod (context) {
}

any.route = '/foo'
any.method = '*'
async function any (context) {
}

// shorthand for ".method = 'PATCH', .route = '/foo'"
shorthand.route = 'PATCH /foo'
async function shorthand (context) {
}

# middleware

Added in 0.1.2.

An array of middleware functions or tuples of middleware functions and arguments to be applied when executing the handler. See the middleware concept doc for more, particularly the section on attaching middleware.

Example use:

// handlers.js
module.exports = { handler }
const { middleware } = require('./boltzmann')

handler.route = 'GET /'
handler.middleware = [
  middleware.session,
  [middleware.applyXFO, 'DENY']
]
async function handler (context) {
}

# route

Added in 0.0.0.

A string representing the route Boltzmann should mount the handler at using find-my-way. .route may include an HTTP verb (e.g., GET /) as a shorthand. See routing for an overview of how to use this property.

The route string may include the following parameter types:

To escape a colon in the route, double it. E.g., /web/::foo.

Example use:

// handlers.js
module.exports = { handler }

handler.route = 'GET /greet/:who'
async function handler (context) {
  return `hello ${context.params.who}`
}

# version

Added in 0.0.0.

A string representing the version of the handler used for routing incoming requests that include an accept-version header.

Automatically adds a vary: accept-version header to all responses from the handler to prevent cache poisoning attacks. If a single route has both versioned and un-versioned handlers, you should add a vary: accept-version header to the un-versioned handler. You can do so using the Boltzmann-provided vary middleware.

Example use:

// handlers.js
module.exports = { handler }

old.route = 'GET /'
old.version = '0.0.0'
async function old (context) {
  return `hello world`
}

// will respond to requests with `accept-version: *`, `accept-version: 1.0.0`,
// or `accept-version: ^1`.
neue.route = 'GET /'
neue.version = '1.0.0'
async function neue (context) {
  return `Hello, world!`
}

# Context

# accepts

Added in 0.0.0.

Content negotiation support for the request. Provided by the accepts package. This property is lazily instantiated on access.

Example use:

myHandler.route = 'GET /foo'
function myHandler(context) {
  switch (context.accepts.type(['json', 'html'])) {
    case 'json':
      return {'hello': 'world'}
    case 'html':
      const res = Buffer.from(`<h1>hello world</h1>`)
      res[Symbol.for('headers')] = {
        'content-type': 'text/html'
      }
      return res
    default:
      // default to text/plain
      return 'hello world'
  }
}

# body

Added in 0.0.0. Changelog
  • Changed in 0.5.0: body may be set by the user and the result will be retained.

A promise for the parsed contents of the request body. This promise resolves to a JavaScript object on success or throws a 422 Unprocessable Entity error when no body parser could handle the request body.

See "accepting user input" for more.

Example use:

myHandler.route = 'POST /foo'
async function myHandler(context) {
  const body = await context.body
  // do something with the body
  if (body.flub) {
    return body.blarp
  }
}

Added in 0.1.1.

A specialized Map instance allowing access to HTTP Cookie information. .cookie supports .get, .set, .delete, .has, and all other Map methods.

.cookie maps cookie names (as strings) to cookie configurations:

{
  httpOnly: Boolean, // defaults to true
  expires: Date,
  maxAge: Number,
  secure: Boolean, // defaults to true in production, false in development mode
  sameSite: true,  // defaults to true
  value: String
}

This configuration information is passed to the cookie package in order to create Set-Cookie headers for outgoing responses.

Boltzmann tracks the state of the cookie map; if any values change or are deleted, Boltzmann automatically generates and attaches a Set-Cookie header to responses.

Incoming cookies don't contain enough information to recreate fields other than .value, so those values are synthesized with defaults.

Example use:

logout.route = 'POST /foo'
async function logout(context) {
  const { value } = context.cookie.get('sessionid') || {}
  if (value) {
    cookie.delete('sessionid')
  }
}

const uuid = require('uuid')

login.route = 'POST /login'
async function login(context) {
  const {username} = await context.body
  const id = uuid.v4()
  context.redisClient.set(id, username)

  context.cookie.set('sessionid', {
    value: username,
    maxAge: 60 // 1 minute! HOW VERY SECURE
  })
}

# handler

Added in 0.5.0.

The handler function to be called once all app-attached middleware has executed. If no route was matched, this property will refer to Context.prototype.handler, which returns a NoMatchError with a 404 Not Found status code. Any middleware attached to the handler will be applied to the function & all handler-attached properties will be normalized.

# headers

Added in 0.0.0.

The HTTP Request Headers as a plain JavaScript object.

This forwards the Node.JS request headers object. All headers are lower-cased and follow the concatenation rules for repeated headers listed in the linked document.

Example use:

logout.route = 'GET /'
async function logout(context) {
  return context.headers['content-type']
}

# host

Added in 0.0.0.

The hostname portion of the Host request header, minus the port. Note that this is the Host header received by the Node.JS process, which may not be the same as the requested host (if, for example, your Boltzmann application is running behind a reverse proxy such as nginx.)

Example use:

host.route = 'GET /'
async function host(context) {
  return context.host // "localhost", if running locally at "localhost:5000"
}

# id

Added in 0.0.0.

A unique string identifier for the request for tracing purposes. The value is drawn from:

  1. x-honeycomb-trace
  2. x-request-id
  3. A generated ship name from Iain M Bank's Culture series (e.g.: "ROU Frank Exchange Of Views")

Example use:

const bole = require('bole')

log.route = 'GET /'
async function log(context) {
  const logger = bole(context.id)
  logger.info('wow what a request')
}

# method

Added in 0.0.0.

The HTTP verb associated with the incoming request, forwarded from the underlying node request object.

Example use:

const assert = require('assert')

assertion.route = 'GET /'
async function assertion(context) {
  assert.equal(context.method, 'GET')
}

# params

Added in 0.0.0.

context.params contains an object mapping URL parameter names to the resolved value for this request. Wildcard matches are available as context.params['*'].

Example use:

parameters.route = 'GET /:foo/bar/:baz'
async function parameters(context) {
  console.log(context.params) // { "foo": "something", "baz": "else" }
}

# postgresClient

Added in 0.0.0.

Requires the --postgres feature.

A lazily-acquired Promise for a postgres Client. Once acquired the same postgres connection is re-used on every subsequent access from a given Context object.

When accessed from a handler responsible for unsafe HTTP methods, the connection automatically runs as part of a transaction. For more, read the "persisting data" chapter.

Example use:

postgres.route = 'GET /users/:name'
async function parameters(context) {
  const client = await context.postgresClient
  const results = await client.query("select * from users where username=$1", [context.params.name])
}

# query

Added in 0.0.0.

query contains the URL search (or "query") parameters for the current request, available as a plain javascript object.

If context.url is set to a new string, context.query is re-calculated.

Warning: Duplicated querystring keys are dropped from this object; only the last key/value pair is available. If you need to preserve exact querystring information, use context.url.searchParams, which is a URLSearchParams object.

Example use:

queries.route = 'GET /'
async function queries(context) {
  if (context.query.foo) {
    // if you requested this handler with "/?foo=1&bar=gary&bar=busey",
    // you would get "busey" as a result
    return context.query.bar
  }
}

# redisClient

Added in 0.0.0.

Requires the --redis feature.

A handy-redis client attached to the context by middleware. A single client is created for the process and shared between request contexts.

Example use:

redis.route = 'GET /'
async function redis(context) {
  const [ok, then] = await context.redisClient.hmget('wow', 'ok', 'then')

  return { ok, then }
}

# remote

Added in 0.0.0.

The remote IP address of the HTTP request sender. Drawn from request.socket.remoteAddress, falling back to request.remoteAddress. This value only represents the immediate connecting socket IP address, so if the application is served through a CDN or other reverse proxy (like nginx) the remote address refers to that host instead of the originating client.

Example use:

remote.route = 'GET /'
async function remote(context) {
  console.log(context.remote) // 127.0.0.1, say
}

# start

Added in 0.0.0.

A Number representing the start of the application request processing, drawn from Date.now().

Example use:

timing.route = 'GET /'
async function timing(context) {
  const ms = Date.now() - context.start
  return `routing this request took ${ms} millisecond${ms === 1 ? '' : 's'}`
}

# session

Added in 0.1.4.

A Promise for a Session object. Session objects are subclasses of the built-in Map class. Session objects provide all of the built-in Map methods, and additionally offers:

You can store any JavaScript object in session storage. However, session storage is serialized as JSON, so rich type information will be lost.

Example use:

sessions.route = 'GET /'
async function sessions(context) {
  const session = await context.session
  const username = session.get('user')

  return username ? 'wow, you are very logged in' : 'not extremely online'
}

logout.route = 'POST /logout'
async function logout(context) {
  const session = await context.session
  session.delete('user')
  session.reissue() // The user is no longer authenticated. Switch the session storage to a new ID.

  return Object.assign(Buffer.from([]), {
    [Symbol.for('status')]: 301,
    [Symbol.for('headers')]: {
      'location': '/'
    }
  })
}

# traceURL

Added in 0.1.4.

Requires the --honeycomb feature.

A URL string suitable for navigating to the [Honeycomb] user interface and displaying the trace from the current request.

Example use:

example.route = 'GET /'
async function example(context) {
  await someComplicatedBusinessLogic()

  console.log(
    'Click on the following URL for details & timings on this request!'
  )

  console.log(context.traceURL) // https://ui.honeycomb.io/trace?some=query&params
  return { some: 'complicated result' }
}

# url

Added in 0.0.0.

A URL instance populated with the host header & incoming request path information. This attribute may be set to a String in order to recalculate the url and query properties.

Example use:

uniformResourceLocation.route = 'GET /'
async function uniformResourceLocation(context) {
  console.log(context.url.pathname) // "/"

  context.url = '/foo/bar?baz=blorp'
  console.log(context.url.pathname) // "/foo/bar"
  console.log(context.query.baz) // "blorp"
}

# Response Symbols

Values returned (or thrown) by a handler or middleware may be annotated with symbols to control response behavior. See the chapter on "handlers" for more. A complete list of symbols and transformations follows.

# Symbol.for('headers')

Added in 0.0.0.

This symbol controls the HTTP response headers sent by Boltzmann along with your handler's return value. It must point to an object. Boltzmann uses the object's keys as header names, and the values as header values.

When a handler or middleware returns a response, Boltzmann enforces certain invariants before passing that response along to the enclosing middleware. In particular, if no content-type header is available on the response headers, Boltzmann adds one. See the section on response transforms for details on which return types produce which content-type values.

Example use:

wow.route = 'GET /'
async function wow(context) {
  return Object.assign(Buffer.from([]), {
    [Symbol.for('headers')]: {
      link: '</foo.css>; rel="stylesheet"'
    }
  })
}

# Symbol.for('status')

Added in 0.0.0.

This symbol controls the HTTP response status code sent by Boltzmann along with your handler's return value. It must point at an integer number.

If not given, Boltzmann infers a status code. See response transforms below for details on which return types produce which status codes.

Example use:

created.route = 'POST /'
async function created(context) {
  return {
    [Symbol.for('status')]: 201,
    [Symbol.for('headers')]: {
      location: '/new-resource-id'
    }
  }
}

errored.route = 'POST /frobs'
async function errored(context) {
  throw Object.assign(new Error('oh wow'), {
    [Symbol.for('status')]: 409,
  })
}

// You may also decorate application error classes
// with symbol information:
class TooCornyError {
  [Symbol.for('status')] = 418
}

errored2.route = 'POST /cobs'
async function errored2(context) {
  throw new TooCornyError('too much corn')
}

# Symbol.for('template')

Added in 0.1.2.

Requires the --templates feature.

This symbol selects a template file to use to render the response as HTML. It must refer to a string value. The template middleware attempts to load a file from one of its configured paths. These paths default to ./templates in the top level.

If the template middleware cannot locate the requested template, it attempts to render a 5xx.html template. If it cannot find that, it renders a fallback 500 Internal Server Error response.

Example use:

html.route = 'GET /'
async function html(context) {
  return {
    some: 'context',
    [Symbol.for('template')]: 'intro.html'
  }
}

# Symbol.for('threw')

Added in 0.0.0.

Boltzmann automatically adds this symbol to thrown values. It signals that the next innermost middleware or handler threw the return value; middleware may change behavior based on its presence or absence. For example: the postgres middleware rolls back transactions if it detects that the handler threw. You can read more about this behavior in the "persisting data" chapter.

Example use:


// a middleware to introspect the "threw" symbol
function didItThrow() {
  return next => async context => {
    const result = await next(context)
    if (result[Symbol.for('threw')]) {
      console.log('it threw!')
    }
    return result
  }
}

example.route = 'GET /'
example.middleware = [didItThrow] // attach our middleware here, ...
async function example(context) {
  if (Math.random() > 0.5) {
    throw new Error('ow I stubbed my toe')
  }

  return 'everything went fine'
}

# Response Transforms

Boltzmann has useful defaults for mapping common return types to HTTP semantics. You can override all of these behaviors using the Boltzmann symbols. These transformations happen between each layer of middleware, as well as between the last middleware and the handler. The return value of next(context) always reflects these transformation.

# "strings"

Added in 0.0.0.

Boltzmann turns strings into [Buffer] instances. If no content-type header was specified, Boltzmann generates one set to text/plain; charset=utf-8.

Example:

function isItABuffer() {
  return next => async context => {
    const result = await next(context)

    if (Buffer.isBuffer(result)) {
      console.log(`it's a buffer!`)
      console.log(String(result)) // "hello world"
    }

    return result
  }
}

example.route = 'GET /'
example.middleware = [isItABuffer] // attach our middleware here, ...
async function example(context) {
  return 'hello world'
}

# undefined, empty return

Added in 0.0.0.

Boltzmann turns empty values turned into [204 No Content] responses. They are cast into empty [Buffer] instances.

Example:

function isItABuffer() {
  return next => async context => {
    const result = await next(context)

    if (Buffer.isBuffer(result)) {
      console.log(result.length) // 0
    }

    return result
  }
}

example1.route = 'GET /'
example1.middleware = [isItABuffer] // attach our middleware here, ...
async function example1(context) {
  // no return
}

example2.route = 'GET /'
example2.middleware = [isItABuffer]
async function example2(context) {
  return // empty return
}

example3.route = 'GET /'
example3.middleware = [isItABuffer]
async function example3(context) {
  return undefined // or null, or "void <expr>"
}

# Node.JS Streams

Added in 0.0.0.

Handlers and middleware can return a [Readable] Node stream. Boltzmann does not resume the stream until all middleware has executed. (User-defined middleware might resume a stream, however.) The stream is piped directly to the Node [response] object.

If no content-type header is present, Boltzmann adds a content-type header value of application/octet-stream.

Example:

const fs = require('fs')

example.route = 'GET /'
async function example(context) {
  return fs.createReadStream(__filename)
}

# JavaScript objects

Added in 0.0.0.

Handlers can return JavaScript objects. After all middleware executes, Boltzmann turns these objects into JSON automatically, then writes them to the response stream. Note that this calls .toJSON() on any member of that object tree. To control serialization, provide a .toJSON() implementation.

If no content-type header is present, Boltzmann adds a content-type header value of application/json; charset=utf-8. If the return object has a Symbol.for('template') attribute defined and the --templates flag is enabled, the content type is text/html; charset=utf-8.

Example:

example.route = 'GET /'
async function example(context) {
  return {
    hello: 'world'
  } // responds with 200 OK; content-type: application/json; charset=utf-8.
    // response body will be `{"hello":"world"}`
}

class MyBusinessLogic {
  publicKnowledge = 'sure'
  secretInternalFacts = 'why not'

  // You can use toJSON() to control serialization of your object.
  // This is built into JSON.stringify()!
  toJSON () {
    const { secretInternalFacts: _, ...rest } = this
    return rest
  }
}
example2.route = 'GET /'
async function example2(context) {
  return new MyBusinessLogic()
}

# Thrown exceptions

Added in 0.0.0.

Handlers can throw exceptions deliberately as well as accidentally. Boltzmann translates exceptions to http semantics and controls what data from the exception is returned to the caller. In particular, in non-development modes, Boltzmann removes the error stack property from its response. Any other property, including message, is forwarded from the exception object to the response.

Notably toJSON() is not called on your exception object.

If a thrown exception does not provide a status code, Boltzmann assigns the 500 Internal Server Error code.

In development mode Boltzmann does not remove the error stack.

Example:

example.route = 'GET /'
async function example(context) {
  // in dev mode, you will see 500 Internal Server Error, content-type is application/json,
  // and the response body will be `{"message":"oh no","stack":"<stack info>"}`.
  // in production you will see `{"message":"oh no"}`.
  throw new Error("oh no")
}

class MyError {
  toJSON() {
    return {"foo": "bar"}
  }
}
example2.route = 'GET /'
async function example2(context) {
  // you will get `{"message":"wow"}`, NOT `{"foo":"bar"}` here.
  return new MyError("wow")
}