Http.Server

An HTTP server for an Elm program: requests arrive as subscription messages, and the program answers each one by sending a response as a command.

listen or listenOnHost starts a Server. onRequest subscribes to its requests, and every Request arrives together with a fresh Response, which the program builds up and sends from its update function:

GotRequest request response ->
    ( model
    , response
        |> Http.Server.setHeader "Content-Type" "text/html"
        |> Http.Server.setBody "<html>Hello there!</html>"
        |> Http.Server.send
    )

A response answers the one request it arrived with. It carries that request's response key, the server's name for a request that is still waiting for its answer. The key is released when the request is answered, when upgradeRequest takes the request over, and when the request's connection goes away. From then on, sending the response writes nothing, so a request is answered at most once however often its response is sent.

The server speaks HTTP/1.0 and HTTP/1.1, HTTP/2 over TLS, or both, as its protocols say (see ListenOptions). Over HTTP/1 a connection is kept alive and serves one request after another; requests a client sends ahead of their answers (pipelining) are handed to the program one at a time, in order, each once the one before it has been answered. Over HTTP/2 several requests on one connection are handed over at once and answered independently.

The server checks every request against its limits and the HTTP rules before the program sees it. A request that breaks them never arrives: the server answers it itself, with a status such as 400, 413 or 431, or over HTTP/2 may reset its stream instead.

Starting a server fails with this module's own Error. On the native backend under Windows no server can be started (listen and listenOnHost fail with NotSupported), and an onRequest subscription stops the program with a fatal error.

Servers

type Server

An HTTP server that listen or listenOnHost started.

A Server stays usable after it has been closed: endpoint still answers, and closing it again does nothing.

type alias ListenOptions =
    { address : Address
    , port_ : Int
    , tls : Maybe ServerOptions
    , protocols : List HttpVersion
    , maxConnections : Maybe Int
    , maxConcurrentStreams : Maybe Int
    , maxBodySize : Int
    , maxHeaderSize : Int
    , keepAliveTimeout : Int
    , headersTimeout : Int
    , requestTimeout : Int
    }

How to run a server: where it listens, whether it uses TLS and HTTP/2, and the limits it holds clients to.

port_ 0 lets the system pick a free port, which endpoint reports.

tls serves HTTPS (and so wss WebSockets) with the given certificate chain and key. Its alpn field is not used: the server offers its protocols instead, h2 for HTTP/2 and http/1.1 for HTTP/1.

protocols are the HTTP versions the server serves, as a set: their order does not matter, repeats are ignored, and a client that can use both HTTP/1 and HTTP/2 is served HTTP/2. Http1_0 and Http1_1 both serve HTTP/1.0 and HTTP/1.1. Http2 serves HTTP/2 to clients that choose it during the TLS handshake, so it requires tls, and TLS 1.2 is then limited to the cipher suites HTTP/2 allows. With HTTP/1, a TLS client that offers neither name, or none at all, is still served HTTP/1.1; without it, such a client is refused. protocols must not be empty.

maxConnections is the most connections open at once. Natively, further clients wait in the operating system's queue of pending connections until one closes; on the JavaScript backend Node drops them.

maxConcurrentStreams is the most HTTP/2 requests, WebSockets included, in progress at once on one connection, and is advertised to the client. Natively, requests the program has not answered yet count even when the client has cancelled them, and at the limit the server processes nothing more from that connection until the program answers one; on the JavaScript backend Node refuses the requests over the limit. Without this limit, natively a client that keeps cancelling its requests is held back only by a limit on cancellations (about 1000 at once, then 33 per second), so a public server should set one.

For both of these limits Nothing means no limit, but Just 0 or a negative value is a limit of 0, which serves nothing. A maxConnections of 0 natively accepts no connection, so clients wait indefinitely, and on the JavaScript backend drops every one. A maxConcurrentStreams of 0 refuses every HTTP/2 request.

maxBodySize and maxHeaderSize are in bytes. A request with a larger body is answered 413, and one with a larger header block 431 (on the JavaScript backend over HTTP/2, a header block over twice maxHeaderSize has its stream reset instead).

keepAliveTimeout, headersTimeout and requestTimeout are in milliseconds, and a value of 0 or less is no timeout. keepAliveTimeout is how long an idle connection stays open between requests. headersTimeout runs from the first byte of a request to the end of its headers and requestTimeout to the end of its body; a request that takes longer is answered 408. Natively, a new connection without TLS that sends nothing is closed after headersTimeout. Over HTTP/2 they apply differently: natively headersTimeout limits only the wait for a new connection's first bytes, and on the JavaScript backend none of the three applies. No timeout limits the program's answer: the connection waits for it.

defaultListenOptions : Address -> Int -> ListenOptions

Returns the options for plain HTTP/1.1 on address and port_, with no connection or stream limits, bodies up to 16 MiB, header blocks up to 64 KiB, a 5 second keep-alive timeout, a 60 second headers timeout and a 300 second request timeout.

Http.Server.defaultListenOptions (Socket.Address.loopback Socket.Address.IPv4) 8080
listen : ListenOptions -> Task Error Server

Starts a server as options say.

let
    options =
        Http.Server.defaultListenOptions (Socket.Address.loopback Socket.Address.IPv4) 8443
in
Http.Server.listen
    { options | tls = Just { certificateChain = cert, privateKey = key, alpn = [] } }

Fails with an Error: for example of kind AddressInUse when the port is taken, CertificateUnusable when the certificate chain or key cannot be used, InvalidPort for a port outside 0 to 65535, and InvalidInput when protocols is empty or has Http2 without tls. A listening server keeps the program running only while an onRequest subscription to it exists or a request it handed over has not been answered.

listenOnHost : String -> Int -> Task Error Server

Starts a server listening on host and port_, with the limits and timeouts of defaultListenOptions, and without TLS.

Http.Server.listenOnHost "localhost" 8080

An address literal such as "127.0.0.1" or "::1" is used as it is. A name is resolved, and the server listens on one of its addresses; natively that is the first IPv4 address that can be used, if there is one. The empty string listens on every interface: natively on 0.0.0.0, so on IPv4 only, and on the JavaScript backend on Node's default, :: where IPv6 is available. Port 0 lets the system pick a free port, which endpoint reports.

A name that cannot be resolved fails with kind HostNotFound, or HostLookupTryAgain when the resolver could not answer for now. A listening server keeps the program running only while an onRequest subscription to it exists or a request it handed over has not been answered.

endpoint : Server -> InetEndpoint

Returns the address and port a server listens on. For port 0 this is the port the system picked. An address the system reports as text that does not parse is given as 0.0.0.0.

close : Server -> Task x ()

Stops a server gracefully, with closeWithin and a deadline of 5 seconds.

closeWithin : Int -> Server -> Task x ()

Stops a server gracefully, closing the connections still open after deadline milliseconds.

The server stops accepting connections at once (the port is free when the task completes) and closes its idle connections. Requests held because nothing subscribed to them (see onRequest) are answered 503. Requests already handed to the program can still be answered until the deadline, over HTTP/1 with Connection: close; then the connections that remain are closed. A request still being received when the server closes can still be handed to a subscription. WebSockets made with upgradeRequest stay open, except one over HTTP/2, which shares its connection and ends with it at the deadline.

Closing a server twice is fine. Once the deadline has passed, nothing of a closed server keeps the program running.

Requests

type alias Request =
    { headers : Dict String String
    , method : Method
    , body : Bytes
    , url : Url
    , version : HttpVersion
    , upgrade : Maybe Upgrade
    }

An HTTP request a server has received.

headers maps each header name, spelled as the client sent it, to its value. Over HTTP/2 names are always lower case, and over HTTP/1 they need not be, so look a header up by every spelling it may have or compare names case-insensitively: names that differ in case are separate entries. When a name is sent more than once with the same spelling, the last value wins. Over HTTP/2 the pseudo-header fields (:method, :path, ...) are not included, and several cookie fields are joined into one with "; ".

body is the complete request body.

url is the absolute URL of the request, made from the Host header (over HTTP/2 the :authority field), or, without one, the host the server was started with (its address for listen, localhost for listenOnHost "") and its port, followed by the request target; a target that is already an absolute URL is used as it is. When that text is not one Url.fromString accepts, for example when the host is a bracketed IPv6 address, url has an empty host and path, and its protocol is Https when the connection uses TLS and Http otherwise.

method is the request's Http.Method.Method, for example GET; a method that has no constructor of its own is UNKNOWN with its name as received, such as UNKNOWN "PROPFIND".

upgrade is the protocol the client asks to switch to, as an Upgrade, such as Just WebSocketUpgrade: the first protocol of the HTTP/1.1 Upgrade header when Connection also names upgrade (natively, only for a request without a body), or HTTP/2's :protocol. Answer a WebSocket request with upgradeRequest. Any response sent instead refuses the upgrade, and over HTTP/1 the connection then closes.

type HttpVersion
    = Http1_0
    | Http1_1
    | Http2

The HTTP version a request was made with, or one a server offers in the protocols of its ListenOptions. A request is made with Http2 only on a server whose protocols include it.

type Upgrade
    = WebSocketUpgrade
    | UnknownUpgrade String

The protocol a request asks to switch to, from its upgrade field.

WebSocketUpgrade is a WebSocket opening request (websocket), to be taken over with upgradeRequest. UnknownUpgrade holds any other protocol, lower-cased, as it was received, for example UnknownUpgrade "h2c". The server switches to no other protocol itself, so the program answers such a request with a normal response, which refuses the upgrade.

The protocol name is compared after lower-casing, so Upgrade: WebSocket is a WebSocketUpgrade too.

bodyAsString : Request -> Maybe String

Returns the request body as a string, or Nothing if it is not valid UTF-8.

bodyAsJson : Decoder a -> Request -> Result Error a

Decodes the request body as JSON. A body that is not valid UTF-8 is decoded as the empty string, so the decoder fails.

requestToString : Request -> String

Returns a one-line description of a request for logs: its method and URL, for example "GET http://localhost:8080/". An UNKNOWN method is written as UNKNOWN(name), so that it cannot be mistaken for one of the methods that have a constructor.

onRequest : Server -> (Request -> Response -> msg) -> Sub msg

Creates a subscription to the requests server receives, each with a fresh Response that answers it.

subscriptions : Model -> Sub Msg
subscriptions model =
    Http.Server.onRequest model.server GotRequest

Every subscription to one server receives every request, all with the same response, so only the first of them to be sent answers it.

Requests that arrive while nothing subscribes to their server are held, and handed out oldest first once a subscription appears. A server holds about maxBodySize × 4 bytes of them, and always at least one. It answers 503 to a request beyond that, and to a request that arrives after the server was closed with nothing subscribed.

Responses

A fresh response has status 200, no headers and an empty body.

type alias Response =
    Response

An HTTP response being built for one request.

A response is obtained only from onRequest, together with the request it answers, and stays tied to that request: the functions below change its status, headers and body, but never which request it answers.

send : Response -> Cmd msg

Creates a command that sends response to the request it answers.

The response is written with its status, its headers in order, one header line per value, and its body:

  • A Content-Length header is always written, except with status 204 or 304, which send no body. The response's own Content-Length, Connection and Transfer-Encoding headers are left out, and so is a header whose name is empty or whose name or value holds a carriage return, line feed or NUL character. On the JavaScript backend a header Node considers invalid, such as one whose name contains a space, is left out as well. A Date header is added unless the response has one.
  • Over HTTP/2 header names are sent lower-cased, and other headers HTTP/2 forbids, such as Upgrade and Keep-Alive, are left out too. On the JavaScript backend, if a header HTTP/2 allows only once, such as Content-Type, has several values (for example from two spellings of its name), every header of the response that has several values keeps only its last one.
  • The answer to a HEAD request has the headers only.
  • An HTML body (see setBodyAsHtml) is serialized now.

Over HTTP/1 the connection then stays open for the client's next request, unless, for example, the client asked to close it (Connection: close, or HTTP/1.0 without keep-alive), the response has a Connection: close header, the server is closing, or the request asked to switch protocols or was a CONNECT.

Sending a response whose request was already answered, was taken over by upgradeRequest, or lost its connection writes nothing.

Status and headers

setStatus : Int -> Response -> Response

Sets the HTTP status code of a response. A status below 200 or above 999 is sent as 500.

response |> Http.Server.setStatus 404
setHeader : String -> String -> Response -> Response

Sets a header on a response, replacing the values of the header with exactly this name and keeping its place, or adding it last. Names are compared exactly, so "content-type" does not replace "Content-Type": the response then has both.

appendHeader : String -> String -> Response -> Response

Adds a value to the header with exactly this name, after the values it already has, or adds the header last. Use this for headers that may appear more than once, such as Set-Cookie.

Body

setBody : String -> Response -> Response

Sets the body of a response to a string; the same as setBodyAsString.

setBodyAsString : String -> Response -> Response

Sets the body of a response to a string, which is sent encoded as UTF-8. The backends differ only for a lone surrogate: the JavaScript backend sends it as U+FFFD, the native one in its 3-byte form, which is not valid UTF-8.

setBodyAsBytes : Bytes -> Response -> Response

Sets the body of a response to bytes, which are sent as they are.

setBodyAsHtml : Node msg -> Response -> Response

Sets the body of a response to an HTML page or fragment built with elm/html (or Svg).

response
    |> Http.Server.setBodyAsHtml
        (Html.node "html" [] [ Html.node "body" [] [ Html.text "Hello!" ] ])
    |> Http.Server.send

When the response is sent:

  • the HTML is written as the text Html.Dom.htmlToString gives, encoded as UTF-8, with the same lone-surrogate difference between the backends as setBodyAsString;
  • <!DOCTYPE html> is written first when the root, seen through Html.map, is an HTML html element;
  • Content-Type: text/html; charset=utf-8 is added unless the response already has a Content-Type header, in any case.

WebSockets

upgradeRequest : Request -> Response -> Task Error Upgrade

Takes over a WebSocket opening request (a Request whose upgrade is Just WebSocketUpgrade, over HTTP/1.1 or HTTP/2) instead of answering it with its Response. The result is a WebSocket.Upgrade, to be answered with WebSocket.accept or WebSocket.reject. The response is used up: sending it later writes nothing.

GotRequest request response ->
    if request.upgrade == Just Http.Server.WebSocketUpgrade then
        ( model
        , Http.Server.upgradeRequest request response
            |> Task.andThen (WebSocket.accept WebSocket.defaultAcceptOptions)
            |> Task.attempt GotWebSocket
        )


    else
        ...

The opening request is checked by WebSocket.accept, which answers a request that breaks the rules with 400 or 426 itself, as for WebSocket.receiveUpgrade. Over HTTP/1.1 the requests the client sent before this one on the same connection are answered first, so the 101 follows their responses.

Over HTTP/2 the request is an extended CONNECT with :protocol websocket, which a server with Http2 allows; the server itself answers any other :protocol with 501. The WebSocket then runs on that one stream of the connection, next to the connection's other requests and WebSockets: accept answers 200 and the stream carries the frames, the close handshake ends the stream, and an aborted WebSocket resets it. Such a WebSocket is one of the connection's streams for maxConcurrentStreams.

The WebSocket does not belong to the server: close leaves it open, except that over HTTP/2 it ends at the close deadline with the connection it shares.

Fails with a WebSocket.Error: with SocketError of kind Socket.InvalidInput when the request does not ask for a WebSocket, in which case the response is untouched and can still be sent; of kind Socket.InvalidInput too when the request was already answered or taken over; and of kind Socket.InvalidInput or Socket.Cancelled when its connection has gone away.

The program keeps running until the Upgrade has been accepted or rejected with WebSocket.accept or WebSocket.reject, or its connection has gone away.

Errors

type Error

A failure to start a server, from listen or listenOnHost.

errorKind says what kind of failure it is, and errorToString describes it for people to read.

This is a separate type from Socket.Error, and from the WebSocket.Error that upgradeRequest fails with, so that a server program matches only on what starting a server can produce.

type ErrorKind
    = AddressInUse
    | AddressNotAvailable
    | HostNotFound
    | HostLookupTryAgain
    | PermissionDenied
    | InvalidPort
    | InvalidInput
    | NotSupported
    | CertificateUnusable String
    | OtherServerError String

What kind of failure an Error is.

  • AddressInUse (EADDRINUSE): the port is already in use, for example by another server.
  • AddressNotAvailable (EADDRNOTAVAIL, EAFNOSUPPORT): the address does not belong to this machine, or the system does not support its family.
  • HostNotFound (ENOTFOUND, EAI_FAIL): the host name given to listenOnHost has no address.
  • HostLookupTryAgain (EAI_AGAIN): the resolver could not answer for now; trying again later may succeed.
  • PermissionDenied (EACCES, EPERM): the operating system refused to listen, for example on a privileged port.
  • InvalidPort (ERR_SOCKET_BAD_PORT): the port is outside 0 to 65535.
  • InvalidInput (EINVAL): the options cannot be used, as empty protocols or Http2 without tls.
  • NotSupported (ENOTSUP): no server can be started here, as on the native backend under Windows.
  • CertificateUnusable reason: the TLS certificate chain or private key cannot be used, for example because it is not PEM or the key does not belong to the certificate. reason is the TLS library's reason, upper-cased with underscores, such as "NO_CERTIFICATES". This is not Socket.Tls.CertificateRejected, which is a peer's certificate failing verification.
  • OtherServerError code: any other failure, with the code the system reported, such as an errno name.
errorKind : Error -> ErrorKind

Returns what kind of failure the error is.

case Http.Server.errorKind error of
    Http.Server.AddressInUse ->
        "the port is taken"


    _ ->
        Http.Server.errorToString error
errorToString : Error -> String

Returns a human readable description of the error: the code the system reported, ": ", and the message, as in "EADDRINUSE: listen EADDRINUSE: address already in use 127.0.0.1:8080".