DocsStandard library

HTTP servers and middleware.

Build a response, route a request, and stream bodies inside connection scopes.

The shared Http module describes headers, bodies, and protocol errors, and Http.Server connects them to a concurrent HTTP/1.1 server. Start with a handler that returns a small byte body, and add streaming only when the payload calls for it.

API reference: Http · Http.Server · Http.Server.Route · Http.GZip

Messages and bodies.

A handler receives a request with a method, a target, headers, and a body reader, and returns a response with a status, headers, and a body. The target includes the query, and the route helpers pull the path out of it.

For a body, use Empty when there’s no payload, or BytesBody for bytes you already have. StreamBody produces its bytes through a writer and uses chunked framing. SizedBody promises a byte count up front, so it must write exactly that many bytes.

Server.

This complete server runs until its stop channel receives a value:

import Async
import Bytes
import Fail
import Http exposing (Body(..))
import Http.Server as Server

hello : Server.Request e ->{e} Server.Response e
hello _ = Server.Response
    { status = 200
    , headers = []
    , body = BytesBody (Bytes.fromString "Hello, Fango!")
    }

main() =
    print (Async.runOutcome {
        Fail.attempt {
            stop = Async.channel 1
            Server.serve (Server.defaultConfig 8000) stop hello
        }
    })

Run it locally and request http://localhost:8000/. It needs native sockets and Async support, so it can’t run in the browser. The complete server example adds routing, echo bodies, compression, and a port argument.

Each connection owns its socket and its scoped buffers. Handle application failures inside the handler. The server translates protocol failures into responses as long as no headers have been written, and a failure after output has begun closes the connection without completing the message.

Routing and GZip.

Http.Server.Route.dispatch checks routes in order. Each route has a method, a path pattern, and a handler, and a :name segment captures a decoded value. A handler for /hello/:name can retrieve it like this:

import Bytes
import Http exposing (Body(..))
import Http.Server as Server
import Http.Server.Route as Route

helloName : List Route.Param -> Server.Request e ->{e} Server.Response e
helloName params _ =
    name = Maybe.withDefault "visitor" (Route.lookup "name" params)
    Server.Response
        { status = 200
        , headers = []
        , body = BytesBody (Bytes.fromString "Hello, #{name}!")
        }

Use helloName as the route’s handler, then give dispatch your list of routes and a fallback. If a path matches but under a different method, the answer is 405, and a path that matches nothing goes to the fallback. Malformed escapes and encoded path separators are rejected, so they can’t change the structure of the route.

Compression wraps a handler you already have. Http.GZip.wrap compresses eligible responses when the client accepts gzip, and adjusts the framing as needed. decodeRequests limit handler handles compressed incoming bodies. Put a bound on their decoded size, not just on the transport limits.

Parsing and framing.

You can try out request parsing on bytes in memory before you run a server:

import Bytes
import Http
import Http.Server as Server
import Http.Server.Route as Route
import Reader

main() =
    fixture = Bytes.fromString "GET /items/42?q=1 HTTP/1.1\r\nHost: example.test\r\n\r\n"
    parsed = handle Reader.withBytes fixture { input ->
        Server.withRequest 4096 16384 1048576 input { request ->
            Route.path request.target
        }
    } on
        Http.invalid _ -> Nothing
    print parsed
Just ("/items/42", True)

The three limits bound the request line, the headers, and the body, each separately. The callback receives a scoped body reader, and the returned Boolean says whether it consumed the whole framed body. An empty GET body counts as already complete.

Keep any response streaming that reads from that body inside the request callback. Conflicting framing fields are errors, so a handler never has to guess which length a peer meant.

Write a streaming body.

A streaming callback receives the output writer and can emit data bit by bit. It runs when the response is serialized, not when the response value is built, and flush sends any pending chunks. For files, Http.fileBody reads the size when it’s constructed and opens the file during writing, so if the file’s size changes, the promised length may no longer be valid.

Let the server supply Content-Length and Transfer-Encoding for you. HEAD sends the headers without running the body, and 204 and 304 responses can’t carry a body.

Errors.

Protocol errors tell apart malformed syntax, exceeded limits, an unsupported transfer coding, invalid application messages, and a body that was deliberately aborted. Before any output has begun, the server can send an error response, and afterwards it has to close the connection. A failed body is never padded out or finished with a misleading final chunk.