std.net.http.server
HTTP 1.1 server support.
This module provides types for implementing custom HTTP 1.1 servers.
A server consists of at least two components:
- An instance of
std.net.http.server.Serverthat listens for incoming requests - A type that implements
std.net.http.server.Handleand handles the incoming request and produces a response
For example, a server that simply displays "hello" as the response for any request is implemented as follows:
import std.net.http.server (Handle, Request, Response, Server)
type async Main {
fn async main {
Server.new(fn { recover App() }).start(8_000).or_panic
}
}
type App {}
impl Handle for App {
fn pub mut handle(request: mut Request) -> Response {
Response.new.string('hello')
}
}
A Server is created using Server.new which takes a closure. This closure
must return a value of uni T where T is some type that implements the
Handle trait. This closure is called for every newly established connection,
and the returned Handle value lives as long as the connection remains
active.
Server.start starts and binds the server to IPv4 address 0.0.0.0 using the
given port number (8000 in this case). You can specify a custom IP address and
port using Server.start_ip, or bind to a Unix domain socket using
Server.start_unix.
In this example App is our application and implements the Handle trait.
This trait has one required method: handle, which takes a request and
returns its response. The response body in this example is set to the string
hello.
For more details, refer to the documentation of the Server and Handle
types.
TLS support
By default a Server starts in plain-text mode. To use TLS, create a Server
using Server.new as usual then call Server.enable_tls to configure it for
TLS connections. A single Server can't handle both plain-text and TLS
requests at the same time. If this is required, you'll need to create a
dedicated Server for plain-text requests and a dedicated Server for TLS
requests.
Keep-alive support
A Server supports (and by default enables) support for HTTP keep-alive
connections, and terminates them if they are idle for too long. The idle
timeout defaults to 60 seconds and can be changed by setting
Server.idle_timeout to a different value.
If a request contains a body that isn't fully read, the connection can't be reused and will instead be closed after the response is written to the client.
Middleware
A common technique used by HTTP frameworks/servers is to build a pipeline of types that handle the request/response cycle, with the individual components commonly referred to as "middleware". Such pipelines are typically created ahead of time (e.g. when a connection is established), then reused.
This module takes a more low-level approach, requiring you to explicitly call/use the appropriate methods/types where necessary. Some of the building blocks this module provides are:
conditional_request: adds support for conditional requests using theIf-None-MatchandIf-Modified-Sinceheadershead_request: adds support forHEADrequests by taking a regular response and turning it into aHEADresponseRangeRequest: a type that adds support for range requests using theRangeandIf-RangeheadersDirectory: a type for serving static files from a directory, complete with theCache-ControlandContent-TypeheadersRequest.same_origin?: a method for protecting against CSRF requests
CSRF protection
By default no protection against CSRF requests is provided. To guard against
such requests, use Request.same_origin? in your Handle.handle
implementation like so:
impl Handle for App {
fn pub mut handle(request: mut Request) -> Response {
# This line should be added before any other code.
if !request.same_origin? { return Response.forbidden }
Response.new.string('hello')
}
}
Logging
The Logger type is used as a basic request logger, it doesn't support
custom log messages. You can use it as follows:
import std.net.http.server (Handle, Logger, Request, Response, Server)
type async Main {
fn async main {
let logger = Logger.new
Server.new(fn { recover App(logger.clone) }).start(8_000).or_panic
}
}
type App {
let @logger: Logger
}
impl Handle for App {
fn pub mut handle(request: mut Request) -> Response {
Response.new.string('hello')
}
fn pub mut response(request: mut Request, response: Response) -> Response {
@logger.log(request, response)
response
}
}
That is: you create a Logger using Logger.new before calling
Server.new, then clone it for each newly established connection. Logging the
request/response is done by overriding the default implementation of
Handle.response to log the request and response data just before we write
the response.
While logging the request in Handle.handle is also fine, moving this code
into Handle.response means we always log the request regardless of
how/where the code returns from Handle.handle.
Signal handling
A Server defines signal handlers for the following signals:
SIGQUIT: stops the server immediatelySIGINT: gracefully stops the serverSIGTERM: gracefully stops the server
The time to wait for a graceful shutdown is controlled by the
Server.shutdown_wait_time.
Sending the SIGINT or SIGTERM signal twice results in an immediate
shutdown of the server.
Routing
Instead of requiring you to construct a complex data structure to use for
determining how to route a request, routing is done by pattern matching
against the value of Request.target. For example:
import std.net.http.server (Handle, Request, Response, Server)
type async Main {
fn async main {
Server.new(fn { recover App() }).start(8_000).or_panic
}
}
type App {}
impl Handle for App {
fn pub mut handle(request: mut Request) -> Response {
match request.target {
case [] -> Response.new.string('Home')
case ['about'] -> Response.new.string('About')
case _ -> Response.not_found
}
}
}
Using this server, requests to / show "Home" as the response, while requests
to /about show "About", while all other requests result in a 404 response.
To also route according to the request method, match against the value of
Request.method:
import std.net.http.server (Get, Handle, Request, Response, Server)
type async Main {
fn async main {
Server.new(fn { recover App() }).start(8_000).or_panic
}
}
type App {}
impl Handle for App {
fn pub mut handle(request: mut Request) -> Response {
match request.target {
case [] -> {
match request.method {
case Get -> Response.new.string('Home')
case _ -> Response.only_allow([Get])
}
}
case ['about'] -> {
match request.method {
case Get -> Response.new.string('About')
case _ -> Response.only_allow([Get])
}
}
case _ -> Response.not_found
}
}
}
This example behaves the same as the previous example, except it only allows
GET requests. Sending a different kind of request (e.g. a HEAD request)
results in a 405 response with the correct Allow header value. The Allow
header is populated based on the std.net.http.Method values passed to
Response.only_allow. The std.net.http.server module provides the following
methods to use so you don't have to explicitly use the Method type:
std.net.http.server.Getstd.net.http.server.Headstd.net.http.server.Poststd.net.http.server.Putstd.net.http.server.Delete
Instead of placing the routing logic directly in the handle method, it's
recommended to create a route method for the Handle type and place the
routing logic in this method:
import std.net.http.server (Get, Handle, Request, Response, Server)
type async Main {
fn async main {
Server.new(fn { recover App() }).start(8_000).or_panic
}
}
type App {
fn mut route(request: mut Request) -> Response {
match request.target {
case [] -> {
match request.method {
case Get -> Response.new.string('Home')
case _ -> Response.only_allow([Get])
}
}
case ['about'] -> {
match request.method {
case Get -> Response.new.string('About')
case _ -> Response.only_allow([Get])
}
}
case _ -> Response.not_found
}
}
}
impl Handle for App {
fn pub mut handle(request: mut Request) -> Response {
route(request)
}
}
Using this approach it's easier to find where the routing logic is located, and makes it easier to modify the response using the various building blocks provided by this module (e.g. by adding support for conditional requests).
Error handling
The implementation of Handle.handle is required to return a Response. This
means that if you call a method that may produce an error (i.e. it returns a
Result[A, B]), you need to somehow convert such values into valid Response
values.
The Response type defines the field Response.error as Option[String],
and is assigned using the Response.error method. You can use this field to
attach error messages that must not be exposed to users (as such errors may
contain sensitive information) but may be recorded somewhere (e.g. by
logging the message).
Recording error messages is best done in a custom implementation of
Handle.response instead of in Handle.handle.
Static files
Serving static files is done using the Directory type, removing the need for
a dedicated server or HTTP proxy to serve static files. For more information,
refer to the documentation of the Directory type.
Generating HTML
Generating HTML is best done using the std.html module and the
Response.html method. For example:
import std.html (Html)
import std.net.http.server (Handle, Request, Response, Server)
type async Main {
fn async main {
Server.new(fn { recover App() }).start(8_000).or_panic
}
}
type App {}
impl Handle for App {
fn pub mut handle(request: mut Request) -> Response {
Response.new.html(Html.new.then(fn (h) { h.p.text('Hello') }))
}
}
This server always responds with the body <p>Hello</p>. For more details,
refer to the documentation of std.html.Html.
Connection hijacking
The Response type supports hijacking of the underlying connection, allowing
you to implement e.g. server-sent sevents or websockets. This is done using
Response.hijack, which takes a closure and passes it the raw HTTP socket.
Once the closure returns, the connection is closed:
import std.net.http.server (Handle, Request, Response, Server)
type async Main {
fn async main {
Server.new(fn { recover App() }).start(8_000).or_panic
}
}
type App {}
impl Handle for App {
fn pub mut handle(request: mut Request) -> Response {
Response.new.hijack(fn (sock) {
let Ok(_) = sock.write('hello') else return
})
}
}
The hijacking API is still experimental and subject to change.
Methods
| Delete | An alias for | |
| Get | An alias for | |
| Head | An alias for | |
| Post | An alias for | |
| Put | An alias for | |
| conditional_request | A method for handling conditional requests. | |
| head_request | A method for handling |
Traits
| Handle | A type that handles a single request and produces its response. |
Types
| Address | A TCP or Unix domain socket address. | |
| Body | The body of a response. | |
| BodyReader | A | |
| Value | BodySize | The size of a body. |
| CacheControl | A type for generating the value of a | |
| Directory | A type for serving static files from a directory. | |
| Event | A server-sent event. | |
| Events | A stream of server-sent events. | |
| File | A file to send to the client. | |
| FileRanges | A type that represents the ranges of a file to send. | |
| FormError | An error produced when parsing a URL encoded form. | |
| Logger | A basic request/response logger. | |
| Atomic | Notifier | A type for notifying a |
| Path | A parsed request path. | |
| RangeRequest | A method for handling range requests. | |
| Request | An HTTP request. | |
| Response | A type for building an HTTP response. | |
| Server | An HTTP server that handles incoming requests. | |
| Socket | A socket for reading requests and writing responses. | |
| WebsocketResponse | A type used for upgrading an HTTP connection to a WebSocket connection. |