Skip to content

Commit

Permalink
Merge pull request #27 from andrewlalis/use-multivalue-map
Browse files Browse the repository at this point in the history
Breaking Change: Version 8.0.0
  • Loading branch information
andrewlalis authored Jan 25, 2024
2 parents d47a0f6 + 7ef9338 commit be24418
Show file tree
Hide file tree
Showing 42 changed files with 1,361 additions and 1,606 deletions.
26 changes: 7 additions & 19 deletions .github/workflows/testing.yml
Original file line number Diff line number Diff line change
Expand Up @@ -96,22 +96,10 @@ jobs:
with:
compiler: ${{ matrix.compiler }}

- name: "Example: file-upload"
working-directory: examples/file-upload
run: dub -q build --single server.d

- name: "Example: handler-testing"
working-directory: examples/handler-testing
run: dub test --single handler.d

- name: "Example: multiple-handlers"
working-directory: examples/multiple-handlers
run: dub -q build

- name: "Example: single-file-server"
working-directory: examples/single-file-server
run: dub -q build --single hello.d

- name: "Example: static-content-server"
working-directory: examples/static-content-server
run: dub -q build --single content_server.d
- name: Test Examples
working-directory: examples
run: rdmd runner.d test

- name: Clean Examples
working-directory: examples
run: rdmd runner.d clean
16 changes: 8 additions & 8 deletions README.md
Original file line number Diff line number Diff line change
@@ -1,13 +1,17 @@
# handy-httpd

![GitHub Actions Workflow Status](https://img.shields.io/github/actions/workflow/status/andrewlalis/handy-httpd/testing.yml?branch=main&style=flat-square&logo=github&label=tests)
![GitHub issues](https://img.shields.io/github/issues/andrewlalis/handy-httpd?style=flat-square)
![DUB Downloads](https://img.shields.io/dub/dt/handy-httpd?style=flat-square&logo=d&logoColor=%23B03931)
![GitHub Tag](https://img.shields.io/github/v/tag/andrewlalis/handy-httpd?style=flat-square&label=version&color=%23B03931)

An extremely lightweight HTTP server for the [D programming language](https://dlang.org/).

## Features
- HTTP/1.1
- [Web Sockets](https://andrewlalis.github.io/handy-httpd/guide/handlers/websocket-handler.html)
- Worker pool for request handling
- [Simple configuration](https://andrewlalis.github.io/handy-httpd/guide/configuration.html)
- High performance
- High performance with interchangeable request processors
- Beginner friendly
- Extensible with custom handlers, exception handlers, and filters
- [Well-documented](https://andrewlalis.github.io/handy-httpd/)
Expand All @@ -17,7 +21,7 @@ An extremely lightweight HTTP server for the [D programming language](https://dl
- Apply filters before and after handling requests with the [FilteredHandler](https://andrewlalis.github.io/handy-httpd/guide/handlers/filtered-handler.html)
- Handle complex URL paths, including path parameters and wildcards, with the [PathHandler](https://andrewlalis.github.io/handy-httpd/guide/handlers/path-handler.html)

## Important Links
## Links
- [Documentation](https://andrewlalis.github.io/handy-httpd/)
- [Examples](https://github.com/andrewlalis/handy-httpd/tree/main/examples)
- [Dub Package Page](https://code.dlang.org/packages/handy-httpd)
Expand All @@ -30,11 +34,7 @@ import handy_httpd;
void main() {
new HttpServer((ref ctx) {
if (ctx.request.url == "/hello") {
ctx.response.writeBodyString("Hello world!");
} else {
ctx.response.setStatus(HttpStatus.NOT_FOUND);
}
ctx.response.writeBodyString("Hello world!");
}).start();
}
```
2 changes: 1 addition & 1 deletion docs/src/guide/configuration.md
Original file line number Diff line number Diff line change
Expand Up @@ -89,5 +89,5 @@ This interval shouldn't need to be very small, unless a high percentage of your
### `enableWebSockets`
| Type | Default Value |
|--- |--- |
| `bool` | `true` |
| `bool` | `false` |
Whether to enable websocket functionality for the server. If set to true, starting the server will also start an additional thread that handles websocket traffic in a nonblocking fashion.
78 changes: 0 additions & 78 deletions docs/src/guide/handlers/path-delegating-handler.md

This file was deleted.

10 changes: 4 additions & 6 deletions docs/src/guide/handling-requests.md
Original file line number Diff line number Diff line change
Expand Up @@ -40,9 +40,9 @@ Each request also contains a `remoteAddress`, which contains the remote socket a

#### Headers and Parameters

The request's headers are available via the `headers` associative array, where each header name is mapped to a single string value. There are no guarantees about which headers may be present, nor their type. It's generally up to the handler to figure out how to deal with them.
The request's headers are available via the [headers](ddoc-handy_httpd.components.request.HttpRequest.headers) *[multivalued map](ddoc-handy_httpd.components.multivalue_map.MultiValueMap)*, 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 list of [QueryParam](ddoc-handy_httpd.components.form_urlencoded.QueryParam)s that were parsed from the URL. For example, in `http://example.com/?x=5`, Handy-Httpd would provide a request whose params are `[QueryParam("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](ddoc-handy_httpd.components.request.HttpRequest.getParamAs) function as a safe way to get a parameter as a specified type, or fallback to a default.
Similarly, the request's [queryParams](ddoc-handy_httpd.components.request.HttpRequest.queryParams) is a *[multivalued map](ddoc-handy_httpd.components.multivalue_map.MultiValueMap)* 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](ddoc-handy_httpd.components.request.HttpRequest.getParamAs) function as a safe way to get a parameter as a specified type, or fallback to a default.

```d
void handle(ref HttpRequestContext ctx) {
Expand All @@ -53,11 +53,9 @@ void handle(ref HttpRequestContext ctx) {

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`.

> ⚠️ Previously, query parameters were accessed through [HttpRequest.params](ddoc-handy_httpd.components.request.HttpRequest.params), but this is deprecated in favor of the query params. This is because the old params implementation didn't accurately follow the official specification; specifically, there can be multiple query parameters with the same name, but different values. An associative string array can't represent that type of data.
#### Path Parameters

If a request is handled by a [PathHandler](handlers/path-handler.md) *(or the now-deprecated [PathDelegatingHandler](handlers/path-delegating-handler.md))*, then its `pathParams` associative array will be populated with any path parameters that were parsed from the URL.
If a request is handled by a [PathHandler](handlers/path-handler.md), then its [pathParams](ddoc-handy_httpd.components.request.HttpRequest.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:

Expand Down Expand Up @@ -91,7 +89,7 @@ Some requests that your server receives may include a body, which is any content
| [readBodyAsJson](ddoc-handy_httpd.components.request.HttpRequest.readBodyAsJson) | Reads the entire request body as a [JSONValue](https://dlang.org/phobos/std_json.html#.JSONValue). |
| [readBodyToFile](ddoc-handy_httpd.components.request.HttpRequest.readBodyToFile) | Reads the entire request body and writes it to a given file. |

Additionally, you can import the [multipart](ddoc-handy_httpd.components.multipart) module to expose the [readBodyAsMultipartFormData](ddoc-handy_httpd.components.multipart.readBodyAsMultipartFormData) function, which is commonly used for handling file uploads.
Additionally, you can use the [multipart](ddoc-handy_httpd.components.multipart) module's [readBodyAsMultipartFormData](ddoc-handy_httpd.components.multipart.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.
Expand Down
11 changes: 8 additions & 3 deletions examples/.gitignore
Original file line number Diff line number Diff line change
@@ -1,3 +1,8 @@
single-file-server/hello
static-content-server/content_server
*.log
# Ignore all artifacts that might be generated.
*

# Except D source files and other stuff that's not auto-generated.
!*.d
!*.md
!*.sh
!*.json
22 changes: 21 additions & 1 deletion examples/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -3,4 +3,24 @@ Inside this directory, you'll find a series of examples which show how Handy-htt

Single-file scripts annotated with a shebang can be run in a unix command-line with `./<script.d>`. On other systems, you should be able to do `dub run --single <script.d>`.

Otherwise, you'll find a `dub.json` file, indicating a dub project, so you can just do `dub run` for those cases.
| Example | Description |
|---|---|
| hello-world | Basic example which shows how to configure and start a server. |
| using-headers | Shows you how to inspect, list, and get the headers from a request. |
| path-handler | Shows you how to use the `PathHandler` to route requests to handlers based on their path, and consume path variables from the request's URL. |
| file-upload | Demonstrates file uploads using multipart/form-data encoding. |
| handler-testing | Shows how you can write unit tests for your request handler functions or classes. |
| static-content-server | Shows how you can use the `FileResolvingHandler` to serve static files from a directory. |
| websocket | Shows how you can enable websocket support and use the `WebSocketHandler` to send and receive websocket messages. |


## Runner Script

A runner script is provided for your convenience. Compile it with `dmd runner.d`, or run directly with `./runner.d`. You can:
- List all examples: `./runner list`
- Clean the examples directory and remove compiled binaries: `./runner clean`
- Select an example to run from a list: `./runner run`
- Run a specific example: `./runner run <example>`
- Run all examples at the same time: `./runner run all`

> Note: When running all examples at the same time, servers will each be given a different port number, starting at 8080.
68 changes: 68 additions & 0 deletions examples/file-upload.d
Original file line number Diff line number Diff line change
@@ -0,0 +1,68 @@
#!/usr/bin/env dub
/+ dub.sdl:
dependency "handy-httpd" path="../"
+/

/**
* This example shows how you can manage basic file-upload mechanics using
* an HTML form and multipart/form-data encoding. In this example, we show a
* simple form, and when the user uploads some files, a summary of the files
* is shown.
*/
module examples.file_upload;

import handy_httpd;
import slf4d;
import handy_httpd.handlers.path_handler;

const indexContent = `
<html>
<body>
<h4>Upload a file!</h4>
<form action="/upload" method="post" enctype="multipart/form-data">
<input type="file" name="file1">
<input type="file" name="file2">
<input type="file" multiple name="other files">
<input type="submit" value="Submit"/>
</form>
</body>
</html>
`;

void main(string[] args) {
ServerConfig cfg = ServerConfig.defaultValues;
if (args.length > 1) {
import std.conv;
cfg.port = args[1].to!ushort;
}
new HttpServer(new PathHandler()
.addMapping(Method.GET, "/**", &serveIndex) // Show the index content to any request.
.addMapping(Method.POST, "/upload", &handleUpload), // And handle uploads only on POST requests to /upload.
cfg
).start();
}

void serveIndex(ref HttpRequestContext ctx) {
ctx.response.writeBodyString(indexContent, "text/html; charset=utf-8");
}

void handleUpload(ref HttpRequestContext ctx) {
MultipartFormData data = readBodyAsMultipartFormData(ctx.request);
string response = "File Upload Summary:\n\n";
foreach (i, MultipartElement element; data.elements) {
import std.format;
string filename = element.filename.isNull ? "NULL" : element.filename.get();
response ~= format!
"Multipart Element %d of %d:\n\tFilename: %s\n\tSize: %d\n"
(
i + 1,
data.elements.length,
filename,
element.content.length
);
foreach (string header, string value; element.headers) {
response ~= format!"\t\tHeader \"%s\": \"%s\"\n"(header, value);
}
}
ctx.response.writeBodyString(response);
}
2 changes: 0 additions & 2 deletions examples/file-upload/.gitignore

This file was deleted.

5 changes: 0 additions & 5 deletions examples/file-upload/README.md

This file was deleted.

54 changes: 0 additions & 54 deletions examples/file-upload/server.d

This file was deleted.

Original file line number Diff line number Diff line change
@@ -1,7 +1,12 @@
/+ dub.sdl:
dependency "handy-httpd" path="../../"
dependency "handy-httpd" path="../"
+/
module handler;

/**
* An example that shows how you can unit-test a request handler. You can run
* this example by calling "dub test --single handler-testing.d".
*/
module examples.handler_testing;

import handy_httpd;
import std.conv : to, ConvException;
Expand Down
8 changes: 0 additions & 8 deletions examples/handler-testing/README.md

This file was deleted.

3 changes: 0 additions & 3 deletions examples/handler-testing/run-tests.sh

This file was deleted.

Loading

0 comments on commit be24418

Please sign in to comment.