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