Search results

There are no results.

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.Server that listens for incoming requests
  • A type that implements std.net.http.server.Handle and 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 the If-None-Match and If-Modified-Since headers
  • head_request: adds support for HEAD requests by taking a regular response and turning it into a HEAD response
  • RangeRequest: a type that adds support for range requests using the Range and If-Range headers
  • Directory: a type for serving static files from a directory, complete with the Cache-Control and Content-Type headers
  • Request.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 immediately
  • SIGINT: gracefully stops the server
  • SIGTERM: 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.Get
  • std.net.http.server.Head
  • std.net.http.server.Post
  • std.net.http.server.Put
  • std.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 std.net.http.Method.Delete.

Get

An alias for std.net.http.Method.Get.

Head

An alias for std.net.http.Method.Head.

Post

An alias for std.net.http.Method.Post.

Put

An alias for std.net.http.Method.Put.

conditional_request

A method for handling conditional requests.

head_request

A method for handling HEAD requests.

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 Read type for reading bytes from a Body.

ValueBodySize

The size of a body.

CacheControl

A type for generating the value of a Cache-Control response header.

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.

AtomicNotifier

A type for notifying a Server that it should shut down.

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.