howdy

howdy is an opinionated web framework on top of ewe.

let assert Ok(_) =
  howdy.new()
  |> howdy.controller(user_controller())
  |> howdy.start

Types

Where a started server is listening. port is the one the operating system chose when the app listens on port 0.

pub type Address {
  Address(ip: String, port: Int)
}

Constructors

  • Address(ip: String, port: Int)
pub opaque type App

Values

pub fn bind(app: App, to address: String) -> App

Set the network interface to listen on. Defaults to "0.0.0.0" (all IPv4 interfaces). Use "127.0.0.1" for local access only. An invalid address panics when the server starts, as in ewe.

pub fn controller(
  app: App,
  controller: controller.Builder(Nil),
) -> App

Mount a controller. Controllers are matched in the order they are added.

pub fn handler(
  app: App,
) -> fn(request.Request(ewe.Connection)) -> response.Response(
  ewe.Body,
)

Compile routes and middleware into a reusable ewe request handler, for running the app under ewe directly with options start does not offer. Pass the result to ewe.new; construct it once and reuse it for every request. This is the one part of howdy’s API that uses ewe’s types.

The routes are kept where every process reads them in place, as start keeps them, so ewe’s processes do not each hold a copy; they are kept until the node stops, so make one handler per server rather than one per request.

To exercise an app without a server, see howdy/testing.

pub fn health_check(app: App, health: health.Health) -> App

Answer health checks from load balancers, container platforms and uptime monitors: GET /up with health.new(). The health check is answered before routing and before the app’s middleware, so nothing the app does to other requests, such as rate limits, guards or redirects, can turn it away, and the logger and tracing do not see it. See howdy/health for checks of the app’s database and other dependencies.

howdy.new()
|> howdy.controller(pages())
|> howdy.health_check(health.new())

Call it again to answer at more than one path, such as a liveness probe that checks nothing and a readiness probe that checks the database. If two share a path, the first added answers.

pub fn listening(app: App, on port: Int) -> App

Set the listening port. Defaults to 8787. Use 0 to let the operating system choose an available port. The last call wins.

pub fn log_disconnects(app: App) -> App

Report clients that went away mid write as crashed connections after all.

By default start installs a node-wide logger filter that drops the supervisor and crash reports the server’s connection pool emits when a client hangs up while being written to, such as a browser tab closed mid WebSocket close handshake. The filter only drops exits raised by the server’s own socket layer with one of its peer-gone reasons; everything else is reported. Call this to keep those reports, for instance while debugging a flaky network.

pub fn middleware(
  app: App,
  middleware: fn(
    context.Context(Nil),
    fn(context.Context(Nil)) -> response.Response(content.Content),
  ) -> response.Response(content.Content),
) -> App

Add middleware that runs for every request the app handles, outside any controller or route middleware. The first added is the outermost.

pub fn new() -> App
pub fn not_found(
  app: App,
  handler: fn(context.Context(Nil)) -> response.Response(
    content.Content,
  ),
) -> App

Answer requests that no route matches with handler, wrapped in the app’s middleware like any route, so logging, CORS and the like see them too. It gets the request with no path parameters, and with a version group, the version the request asked for if it named one.

Without it such a request gets an empty 404 that no middleware sees. A path some route matches with another method still gets its 405, and a catch-all route such as static.serve("/", ...) matches first, so its own 404s are the controller’s and pass through middleware as usual.

howdy.new()
|> howdy.controller(pages())
|> howdy.not_found(fn(ctx) {
  service.error_response(ctx, service.NotFound("no such page"))
})
pub fn routes(app: App) -> List(controller.Route)

The routes of every controller mounted so far, in matching order, each wrapped in its controller’s middleware but not the app’s. Routes in a version group are not included. For packages that describe an app’s routes, such as howdy_openapi; call it once every controller is mounted.

pub fn shutdown_timeout(app: App, milliseconds: Int) -> App

Set how long each open connection gets to finish when the server is stopped, in milliseconds. 15 seconds by default.

HTTP/1 connections finish the request they are serving, WebSockets are sent a going-away close frame and HTTP/2 connections send GOAWAY and wait for their open streams. A connection still open once the time is up is closed. This applies to stop and to a shutdown of the supervision tree the server runs in; see supervised.

pub fn start(
  app: App,
) -> Result(actor.Started(Address), actor.StartError)

Start the app’s HTTP server. Defaults to 0.0.0.0:8787; override with bind and listening. Prints a howdy banner with the framework version and bound URL once listening.

Returns the server’s process and the Address it is listening on. The server is linked to the calling process: if that process exits, so does the server, and if the server exits abnormally, so does the caller. That is fine for a script that calls process.sleep_forever() afterwards, but an app that also runs a database pool, a mail outbox or feature flags should put them all under a supervisor with supervised instead, so a crash in one is restarted rather than ending the program.

Stop the server with stop, which lets open connections finish.

Clients that disconnect while the server is still writing to them, such as a browser tab closed mid WebSocket, are not logged as crashes; see log_disconnects to change that.

pub fn start_application(
  tree: static_supervisor.Builder,
  name name: String,
) -> Result(process.Pid, String)

Start a supervision tree as an OTP application called name, and return the pid of its root supervisor.

This is how a howdy app should run in production. The tree holds the server from supervised alongside anything else the app keeps running, such as a database pool, feature flags or a mail outbox, each restarted on its own if it crashes. Because the tree belongs to an application, the node’s own shutdown stops it in order: on SIGTERM, or init:stop(), the server stops listening and gives open connections shutdown_timeout to finish before the VM halts. A tree started from main without this is simply killed with the VM.

name must not be the name of a package the app already loads, so pick one like "my_app_server". Keep the calling process alive afterwards, with process.sleep_forever() from main.

import gleam/otp/static_supervisor as supervisor

pub fn main() -> Nil {
  let assert Ok(_) =
    supervisor.new(supervisor.OneForOne)
    |> supervisor.add(howdy.supervised(app()))
    |> howdy.start_application(name: "my_app_server")
  process.sleep_forever()
}

Stop it again with stop_application.

pub fn start_with(
  app: App,
  handler: fn(request.Request(context.Body)) -> response.Response(
    content.Content,
  ),
) -> Result(actor.Started(Address), actor.StartError)

Start a server for app that answers requests with handler instead of the app’s own. The address, port and banner come from app. This is what development tooling such as howdy_dev uses to wrap an app.

pub fn stop(server: process.Pid) -> Nil

Stop a server started with start and wait for it to finish.

The listener closes first so no new connections arrive, then each open connection gets shutdown_timeout to finish: HTTP/1 requests complete, WebSockets are sent a going-away close frame and HTTP/2 streams are told GOAWAY. Returns once every connection is gone and the port is free. The calling process is unlinked from the server first, so it is not taken down by the stop. A server still running a minute after the default drain timeout is killed rather than waited on forever.

pub fn stop_application(name: String) -> Nil

Stop an application started with start_application and wait for its tree to finish, draining the server’s connections on the way.

pub fn stop_within(
  server: process.Pid,
  shutdown_timeout milliseconds: Int,
) -> Nil

Like stop, for a server whose shutdown_timeout is not the default: the wait for a wedged server is measured from it.

pub fn supervised(
  app: App,
) -> supervision.ChildSpecification(Address)

A child specification for the app’s HTTP server, to add to a supervision tree with static_supervisor.add. The child’s data is the Address the server listens on, and the supervisor restarts the server if it exits.

import gleam/otp/static_supervisor as supervisor

let assert Ok(_) =
  supervisor.new(supervisor.OneForOne)
  |> supervisor.add(howdy.supervised(app))
  |> supervisor.start

Connections are drained for shutdown_timeout when the supervisor stops the server, which happens when the tree is stopped explicitly or, for a tree started by an OTP application, when the node shuts down. A tree started from main and left running with process.sleep_forever() is killed with the VM instead, so an app that wants a clean stop on SIGTERM should start its tree from an application module; see the supervision guide.

pub fn tls(app: App, cert cert: String, key key: String) -> App

Serve HTTPS using PEM-encoded certificate and key files. Browsers only speak HTTP/2 over TLS, so this is also what enables HTTP/2 for them; it is offered through ALPN alongside HTTP/1.1. For in-memory certificates use tls_pem. Without TLS, HTTP/2 is still served to clients that open with the HTTP/2 preface, such as curl --http2-prior-knowledge.

pub fn tls_pem(
  app: App,
  cert cert: BitArray,
  key key: BitArray,
) -> App

Serve HTTPS using a PEM-encoded certificate and key held in memory, such as ones read from a secret store. Otherwise the same as tls.

pub const version: String

The howdy framework version. Keep in sync with gleam.toml.

pub fn version_group(app: App) -> option.Option(version.Group)

The version group mounted with versions, if any. For packages that describe an app’s routes, such as howdy_openapi.

pub fn versions(app: App, group: version.Group) -> App

Mount a version group from howdy/version. Unversioned controllers are matched first; the group answers whatever they do not. An app has at most one group, so mounting a second panics.

✨ Search Document