# Handling Requests
As you've seen in the introduction, you can create a new server with a function that takes in an HttpRequestContext. This function serves as a handler, that handles incoming requests. In fact, that function is just a shorthand way of making an instance of the HttpRequestHandler interface.
The HttpRequestHandler interface defines a single method: handle, that takes a request context and performs everything that's needed to respond to that request.
Usually, to correctly respond to a request, you need to:
- Read information from the HTTP request.
- Do some logic based on that information.
- Set the response status and headers.
- Send the response body content (if there's any to send).
# The Request Context
The HttpRequestContext that's provided to the handler contains all the information needed to respond to the request. This includes:
- The HttpRequest that was received.
- The HttpResponse that will be sent.
- The HttpServer that received the request.
- The ServerWorkerThread that's processing the request.
# Request
Information about the request itself is available via the request context's request
attribute, which is an HttpRequest struct containing the basic information about the parsed request, as well as methods for consuming the rest of the request body (if applicable).
# Basic Properties
Every HTTP request has a string url
and method
; for example, GET http://example.com/index.html
or POST http://example.com/data
. The request also contains an integer ver
version identifier, which generally should always be set to 1
since Handy-Httpd doesn't support HTTP 2 or higher.
void handle(ref HttpRequestContext ctx) {
if (ctx.request.url == "/users" && ctx.request.method == Method.GET) {
// Retrieve list of users.
}
}
The url
is relative to the base hostname of the server, so if you do GET http://localhost:8080/data
, the request's url
will be /data
.
Each request also contains a remoteAddress
, which contains the remote socket address of the client that issued the request. Keep in mind that this might be null, and probably will be for most unit tests. See std.socket : Address (opens new window) in Phobos for more information on how to work with addresses.
# Headers and Parameters
The request's headers are available via the headers multivalued map, where each header name is mapped to one or more string values. There are no guarantees about which headers may be present, and it's generally up to the handler to do this.
Similarly, the request's queryParams is a multivalued map containing all query parameters that were parsed from the URL. For example, in http://example.com/?x=5
, Handy-Httpd would provide a request whose params are ["x": ["5"]]
. Like the headers, no guarantee is made about what params are present, or what type they are. However, you can use the getParamAs function as a safe way to get a parameter as a specified type, or fallback to a default.
void handle(ref HttpRequestContext ctx) {
int page = ctx.request.getParamAs!int("page", 1);
// Do other stuff below...
}
In the above snippet, if the user requests https://your-site.com/search?page=24
, then page
will be set to 24. However, if a user requests https://your-site.com/search?page=blah
, or doesn't provide a page at all, page
will be set to 1
.
# Path Parameters
If a request is handled by a PathHandler, then its pathParams associative array will be populated with any path parameters that were parsed from the URL.
The easiest way to understand this behavior is through an example. Suppose we define our top-level PathHandler with the following mapping, so that a userSettingsHandler
will handle requests to that endpoint:
auto handler = new PathHandler();
handler.addMapping("/users/:userId:ulong/settings/:setting", &userSettingsHandler);
Then in our userSettingsHandler
we can retrieve the path parameters like so:
void handle(ref HttpRequestContext ctx) {
ulong userId2 = ctx.request.getPathParamAs!ulong("userId");
string setting = ctx.request.pathParams["setting"];
// Do stuff for this user...
}
For more information about the PathHandler, please see the dedicated page on this topic.
# Body Content
Some requests that your server receives may include a body, which is any content that comes after the URL and headers of the request. The HttpRequest offers the following methods for reading the body of the request:
Method | Description |
---|---|
readBody | Reads the request body, and writes it to a given output stream. Unless you explicitly enable infinite reading, it will respect the request's Content-Length header, and if no such header is present, nothing will be read. |
readBodyAsBytes | Reads the entire request body to a byte array. |
readBodyAsString | Reads the entire request body to a string. |
readBodyAsFormUrlEncoded | Reads the entire request body as form-urlencoded (opens new window) key-value pairs. |
readBodyAsJson | Reads the entire request body as a JSONValue (opens new window). |
readBodyToFile | Reads the entire request body and writes it to a given file. |
Additionally, you can use the multipart module's readBodyAsMultipartFormData function, which is commonly used for handling file uploads.
⚠️ While Handy-Httpd doesn't force you to limit the amount of data you read, please be careful when reading an entire request body at once, like with
readBodyAsString
. This will load the entire request body into memory, and will crash your program if the body is too large.
Sometimes, the body content of a request will be encoded if the Transfer-Encoding=chunked
header is provided. In that case, Handy-Httpd will automatically wrap the underlying input stream with one that reads the chunked encoding and provides you with the raw data. In short, Handy-Httpd will manage chunked-encoded requests for you. However, it will not automatically apply chunked encoding to your server's responses. If you'd like to send chunked-encoded responses, consider using the ChunkedEncodingOutputStream (opens new window) from the streams (opens new window) library to do so, since Handy-Httpd already includes it as a dependency.
# Response
Besides the request itself, the request context also contains the HttpResponse to which your handler will write its response data. Usually, you'll follow the standard sequence of events mentioned above, and you'll:
- Set the response status and headers.
- Send the response body content (if necessary).
What this might look like in practice is shown in the example below:
void handle(ref HttpRequestContext ctx) {
// Do logic on request.
ctx.response.status = HttpStatus.CREATED;
ctx.response.addHeader("X-MY-TOKEN", "abc");
// Calling `writeBodyString` will automatically flush the status and headers to the socket.
ctx.response.writeBodyString("{\"id\": 1}", "application/json");
}
# Status and Headers
The first thing you should do when responding to a request is to send a status, and headers. The response's status
can be set according to a value from the HttpStatus enum, which lists every valid HTTP status code and its associated textual representation.
You can add headers via addHeader(string name, string value).
⚠️ Setting the status and headers is only possible before they've been flushed, i.e. before any body content is sent.
# Writing the Response Body
After setting a status and headers, you can write the response body. This can be done with one of the methods provided by the response:
Method | Description |
---|---|
writeBody | Writes the response body using data taken from an input stream of bytes. The size and content type must be explicitly specified before anything is written. |
writeBodyBytes | Writes the given bytes to the response body. You can optionally specify a content type, or it'll default to application/octet-stream . |
writeBodyString | Writes the given text to the response body. You can optionally specify a content type, or it'll default to text/plain; charset=utf-8 . |
# IO
For IO operations while handling requests, Handy-Httpd uses the streams (opens new window) library. It offers a simple interface for input and output stream primitives, and is generally a bit more extensible than the ranges that are present in the Phobos standard library.
Input streams read from some underlying resource. In the case of HTTP, that mostly means that when we receive a request, we construct a SocketInputStream
around the TCP socket we're using for the connection.
Conversely, output streams write to some underlying resource, and again, since we're talking HTTP, that means we use a SocketOutputStream
to write bytes to the TCP socket.
The read...
and write...
methods of the HttpRequest and the HttpResponse, respectively, are just wrappers around the underlying readFromStream
and writeToStream
methods of the input and output streams.
For a more in-depth explanation, please read the documentation available at the streams library's source.