Note

All examples in this document are using Server in debug mode. This mode is useful for development, but it is not recommended to use it in production. More about Debug mode at the end of Examples section.

Different ways of starting the server

There are several ways to start the server on CircuitPython, mostly depending on the device you are using and whether you have access to external network.

Functionally, all of them are the same, not features of the server are limited or disabled in any way.

Below you can find examples of different ways to start the server:

CPython usage

Library can also be used in CPython, no changes other than changing the socket_source are necessary.

examples/httpserver_cpython.py
 1# SPDX-FileCopyrightText: 2024 Michał Pokusa
 2#
 3# SPDX-License-Identifier: Unlicense
 4
 5import socket
 6
 7from adafruit_httpserver import Server, Request, Response
 8
 9
10pool = socket
11server = Server(pool, "/static", debug=True)
12
13
14@server.route("/")
15def base(request: Request):
16    """
17    Serve a default static plain text message.
18    """
19    return Response(request, "Hello from the CircuitPython HTTP Server!")
20
21
22# Ports below 1024 are reserved for root user only.
23# If you want to run this example on a port below 1024, you need to run it as root (or with `sudo`).
24server.serve_forever("0.0.0.0", 5000)

Serving static files

It is possible to serve static files from the filesystem. In this example we are serving files from the /static directory.

In order to save memory, we are unregistering unused MIME types and registering additional ones. More about MIME types.

examples/httpserver_static_files_serving.py
 1# SPDX-FileCopyrightText: 2023 Michał Pokusa
 2#
 3# SPDX-License-Identifier: Unlicense
 4
 5
 6import socketpool
 7import wifi
 8
 9from adafruit_httpserver import Server, MIMETypes
10
11
12MIMETypes.configure(
13    default_to="text/plain",
14    # Unregistering unnecessary MIME types can save memory
15    keep_for=[".html", ".css", ".js", ".png", ".jpg", ".jpeg", ".gif", ".ico"],
16    # If you need to, you can add additional MIME types
17    register={".foo": "text/foo", ".bar": "text/bar"},
18)
19
20pool = socketpool.SocketPool(wifi.radio)
21server = Server(pool, "/static", debug=True)
22
23# You don't have to add any routes, by default the server will serve files
24# from it's root_path, which is set to "/static" in this example.
25
26# If you don't set a root_path, the server will not serve any files.
27
28server.serve_forever(str(wifi.radio.ipv4_address))

You can also serve a specific file from the handler. By default FileResponse looks for the file in the server’s root_path directory (/default-static-directory in the example below), but you can change it manually in every FileResponse (to e.g. /other-static-directory, as in example below).

By doing that, you can serve files from multiple directories, and decide exactly which files are accessible.

examples/httpserver_handler_serves_file.py
 1# SPDX-FileCopyrightText: 2022 Dan Halbert for Adafruit Industries, Michał Pokusa
 2#
 3# SPDX-License-Identifier: Unlicense
 4
 5
 6import socketpool
 7import wifi
 8
 9from adafruit_httpserver import Server, Request, FileResponse
10
11
12pool = socketpool.SocketPool(wifi.radio)
13server = Server(pool, "/default-static-folder", debug=True)
14
15
16@server.route("/home")
17def home(request: Request):
18    """
19    Serves the file /other-static-folder/home.html.
20    """
21
22    return FileResponse(request, "home.html", "/other-static-folder")
23
24
25server.serve_forever(str(wifi.radio.ipv4_address))
www/home.html
 1<html lang="en">
 2    <head>
 3        <meta charset="UTF-8">
 4        <meta http-equiv="X-UA-Compatible" content="IE=edge">
 5        <meta name="viewport" content="width=device-width, initial-scale=1.0">
 6        <title>Adafruit HTTPServer</title>
 7    </head>
 8    <body>
 9        <p>Hello from the <strong>CircuitPython HTTP Server!</strong></p>
10    </body>
11</html>

Tasks between requests

If you want your code to do more than just serve web pages, use the .start()/.poll() methods as shown in this example.

Between calling .poll() you can do something useful, for example read a sensor and capture an average or a running total of the last 10 samples.

.poll() return value can be used to check if there was a request and if it was handled.

examples/httpserver_start_and_poll.py
 1# SPDX-FileCopyrightText: 2022 Dan Halbert for Adafruit Industries
 2#
 3# SPDX-License-Identifier: Unlicense
 4
 5import socketpool
 6import wifi
 7
 8from adafruit_httpserver import (
 9    Server,
10    REQUEST_HANDLED_RESPONSE_SENT,
11    Request,
12    FileResponse,
13)
14
15
16pool = socketpool.SocketPool(wifi.radio)
17server = Server(pool, "/static", debug=True)
18
19
20@server.route("/")
21def base(request: Request):
22    """
23    Serve the default index.html file.
24    """
25    return FileResponse(request, "index.html")
26
27
28# Start the server.
29server.start(str(wifi.radio.ipv4_address))
30
31while True:
32    try:
33        # Do something useful in this section,
34        # for example read a sensor and capture an average,
35        # or a running total of the last 10 samples
36
37        # Process any waiting requests
38        pool_result = server.poll()
39
40        if pool_result == REQUEST_HANDLED_RESPONSE_SENT:
41            # Do something only after handling a request
42            pass
43
44        # If you want you can stop the server by calling server.stop() anywhere in your code
45    except OSError as error:
46        print(error)
47        continue

If you need to perform some action periodically, or there are multiple tasks that need to be done, it might be better to use asyncio module to handle them, which makes it really easy to add new tasks without needing to manually manage the timing of each task.

asyncio is not included in CircuitPython by default, it has to be installed separately.

examples/httpserver_start_and_poll_asyncio.py
 1# SPDX-FileCopyrightText: 2023 Michał Pokusa
 2#
 3# SPDX-License-Identifier: Unlicense
 4
 5from asyncio import create_task, gather, run, sleep as async_sleep
 6import socketpool
 7import wifi
 8
 9from adafruit_httpserver import (
10    Server,
11    REQUEST_HANDLED_RESPONSE_SENT,
12    Request,
13    FileResponse,
14)
15
16
17pool = socketpool.SocketPool(wifi.radio)
18server = Server(pool, "/static", debug=True)
19
20
21@server.route("/")
22def base(request: Request):
23    """
24    Serve the default index.html file.
25    """
26    return FileResponse(request, "index.html")
27
28
29# Start the server.
30server.start(str(wifi.radio.ipv4_address))
31
32
33async def handle_http_requests():
34    while True:
35        # Process any waiting requests
36        pool_result = server.poll()
37
38        if pool_result == REQUEST_HANDLED_RESPONSE_SENT:
39            # Do something only after handling a request
40            pass
41
42        await async_sleep(0)
43
44
45async def do_something_useful():
46    while True:
47        # Do something useful in this section,
48        # for example read a sensor and capture an average,
49        # or a running total of the last 10 samples
50        await async_sleep(1)
51
52        # If you want you can stop the server by calling server.stop() anywhere in your code
53
54
55async def main():
56    await gather(
57        create_task(handle_http_requests()),
58        create_task(do_something_useful()),
59    )
60
61
62run(main())

Server with MDNS

It is possible to use the MDNS protocol to make the server accessible via a hostname in addition to an IP address. It is worth noting that it takes a bit longer to get the response from the server when accessing it via the hostname.

In this example, the server is accessible via the IP and http://custom-mdns-hostname.local:5000/. On some routers it is also possible to use http://custom-mdns-hostname:5000/, but this is not guaranteed to work.

examples/httpserver_mdns.py
 1# SPDX-FileCopyrightText: 2022 Michał Pokusa
 2#
 3# SPDX-License-Identifier: Unlicense
 4
 5import mdns
 6import socketpool
 7import wifi
 8
 9from adafruit_httpserver import Server, Request, FileResponse
10
11
12mdns_server = mdns.Server(wifi.radio)
13mdns_server.hostname = "custom-mdns-hostname"
14mdns_server.advertise_service(service_type="_http", protocol="_tcp", port=5000)
15
16pool = socketpool.SocketPool(wifi.radio)
17server = Server(pool, "/static", debug=True)
18
19
20@server.route("/")
21def base(request: Request):
22    """
23    Serve the default index.html file.
24    """
25
26    return FileResponse(request, "index.html", "/www")
27
28
29server.serve_forever(str(wifi.radio.ipv4_address))

Get CPU information

You can return data from sensors or any computed value as JSON. That makes it easy to use the data in other applications.

If you want to use the data in a web browser, it might be necessary to enable CORS. More info: https://developer.mozilla.org/en-US/docs/Web/HTTP/CORS

examples/httpserver_cpu_information.py
 1# SPDX-FileCopyrightText: 2022 Dan Halbert for Adafruit Industries
 2#
 3# SPDX-License-Identifier: Unlicense
 4
 5import microcontroller
 6import socketpool
 7import wifi
 8
 9from adafruit_httpserver import Server, Request, JSONResponse
10
11
12pool = socketpool.SocketPool(wifi.radio)
13server = Server(pool, debug=True)
14
15# (Optional) Allow cross-origin requests.
16server.headers = {
17    "Access-Control-Allow-Origin": "*",
18}
19
20
21@server.route("/cpu-information", append_slash=True)
22def cpu_information_handler(request: Request):
23    """
24    Return the current CPU temperature, frequency, and voltage as JSON.
25    """
26
27    data = {
28        "temperature": microcontroller.cpu.temperature,
29        "frequency": microcontroller.cpu.frequency,
30        "voltage": microcontroller.cpu.voltage,
31    }
32
33    return JSONResponse(request, data)
34
35
36server.serve_forever(str(wifi.radio.ipv4_address))

Handling different methods

On every server.route() call you can specify which HTTP methods are allowed. By default, only GET method is allowed.

You can pass a list of methods or a single method as a string.

It is recommended to use the the values in adafruit_httpserver.methods module to avoid typos and for future proofness.

If you want to route a given path with and without trailing slash, use append_slash=True parameter.

In example below, handler for /api and /api/ route will be called when any of GET, POST, PUT, DELETE methods is used.

examples/httpserver_methods.py
 1# SPDX-FileCopyrightText: 2023 Michał Pokusa
 2#
 3# SPDX-License-Identifier: Unlicense
 4
 5import socketpool
 6import wifi
 7
 8from adafruit_httpserver import Server, Request, JSONResponse, GET, POST, PUT, DELETE
 9
10
11pool = socketpool.SocketPool(wifi.radio)
12server = Server(pool, debug=True)
13
14objects = [
15    {"id": 1, "name": "Object 1"},
16]
17
18
19@server.route("/api", [GET, POST, PUT, DELETE], append_slash=True)
20def api(request: Request):
21    """
22    Performs different operations depending on the HTTP method.
23    """
24
25    # Get objects
26    if request.method == GET:
27        return JSONResponse(request, objects)
28
29    # Upload or update objects
30    if request.method in [POST, PUT]:
31        uploaded_object = request.json()
32
33        # Find object with same ID
34        for i, obj in enumerate(objects):
35            if obj["id"] == uploaded_object["id"]:
36                objects[i] = uploaded_object
37
38                return JSONResponse(
39                    request, {"message": "Object updated", "object": uploaded_object}
40                )
41
42        # If not found, add it
43        objects.append(uploaded_object)
44        return JSONResponse(
45            request, {"message": "Object added", "object": uploaded_object}
46        )
47
48    # Delete objects
49    if request.method == DELETE:
50        deleted_object = request.json()
51
52        # Find object with same ID
53        for i, obj in enumerate(objects):
54            if obj["id"] == deleted_object["id"]:
55                del objects[i]
56
57                return JSONResponse(
58                    request, {"message": "Object deleted", "object": deleted_object}
59                )
60
61        # If not found, return error
62        return JSONResponse(
63            request, {"message": "Object not found", "object": deleted_object}
64        )
65
66    # If we get here, something went wrong
67    return JSONResponse(request, {"message": "Something went wrong"})
68
69
70server.serve_forever(str(wifi.radio.ipv4_address))

Change NeoPixel color

There are several ways to pass data to the handler function:

  • In your handler function you can access the query/GET parameters using request.query_params

  • You can also access the POST data directly using request.body or if you data is in JSON format, you can use request.json() to parse it into a dictionary

  • Alternatively for short pieces of data you can use URL parameters, which are described later in this document For more complex data, it is recommended to use JSON format.

All of these approaches allow you to pass data to the handler function and use it in your code.

For example by going to /change-neopixel-color?r=255&g=0&b=0 or /change-neopixel-color/255/0/0 you can change the color of the NeoPixel to red. Tested on ESP32-S2 Feather.

examples/httpserver_neopixel.py
 1# SPDX-FileCopyrightText: 2022 Michał Pokusa
 2#
 3# SPDX-License-Identifier: Unlicense
 4
 5import board
 6import neopixel
 7import socketpool
 8import wifi
 9
10from adafruit_httpserver import Server, Route, as_route, Request, Response, GET, POST
11
12
13pool = socketpool.SocketPool(wifi.radio)
14server = Server(pool, "/static", debug=True)
15
16pixel = neopixel.NeoPixel(board.NEOPIXEL, 1)
17
18
19# This is the simplest way to register a route. It uses the Server object in current scope.
20@server.route("/change-neopixel-color", GET)
21def change_neopixel_color_handler_query_params(request: Request):
22    """Changes the color of the built-in NeoPixel using query/GET params."""
23
24    # e.g. /change-neopixel-color?r=255&g=0&b=0
25
26    r = request.query_params.get("r") or 0
27    g = request.query_params.get("g") or 0
28    b = request.query_params.get("b") or 0
29
30    pixel.fill((int(r), int(g), int(b)))
31
32    return Response(request, f"Changed NeoPixel to color ({r}, {g}, {b})")
33
34
35# This is another way to register a route. It uses the decorator that converts the function into
36# a Route object that can be imported and registered later.
37@as_route("/change-neopixel-color/form-data", POST)
38def change_neopixel_color_handler_post_form_data(request: Request):
39    """Changes the color of the built-in NeoPixel using POST form data."""
40
41    data = request.form_data  # e.g. r=255&g=0&b=0 or r=255\r\nb=0\r\ng=0
42    r, g, b = data.get("r", 0), data.get("g", 0), data.get("b", 0)
43
44    pixel.fill((int(r), int(g), int(b)))
45
46    return Response(request, f"Changed NeoPixel to color ({r}, {g}, {b})")
47
48
49def change_neopixel_color_handler_post_json(request: Request):
50    """Changes the color of the built-in NeoPixel using JSON POST body."""
51
52    data = request.json()  # e.g {"r": 255, "g": 0, "b": 0}
53    r, g, b = data.get("r", 0), data.get("g", 0), data.get("b", 0)
54
55    pixel.fill((r, g, b))
56
57    return Response(request, f"Changed NeoPixel to color ({r}, {g}, {b})")
58
59
60# You can always manually create a Route object and import or register it later.
61# Using this approach you can also use the same handler for multiple routes.
62post_json_route = Route(
63    "/change-neopixel-color/json", POST, change_neopixel_color_handler_post_json
64)
65
66
67def change_neopixel_color_handler_url_params(
68    request: Request, r: str = "0", g: str = "0", b: str = "0"
69):
70    """Changes the color of the built-in NeoPixel using URL params."""
71
72    # e.g. /change-neopixel-color/255/0/0
73
74    pixel.fill((int(r), int(g), int(b)))
75
76    return Response(request, f"Changed NeoPixel to color ({r}, {g}, {b})")
77
78
79# Registering Route objects
80server.add_routes(
81    [
82        change_neopixel_color_handler_post_form_data,
83        post_json_route,
84        # You can also register a inline created Route object
85        Route(
86            path="/change-neopixel-color/<r>/<g>/<b>",
87            methods=GET,
88            handler=change_neopixel_color_handler_url_params,
89        ),
90    ]
91)
92
93
94server.serve_forever(str(wifi.radio.ipv4_address))

Templates

With the help of the adafruit_templateengine library, it is possible to achieve somewhat of a server-side rendering of HTML pages.

Instead of using string formatting, you can use templates, which can include more complex logic like loops and conditionals. This makes it very easy to create dynamic pages, witout using JavaScript and exposing any API endpoints.

Templates also allow splitting the code into multiple files, that can be reused in different places. You can find more information about the template syntax in the adafruit_templateengine documentation.

examples/directory_listing.tpl.html
 1{% exec path = context.get("path") %}
 2{% exec items = context.get("items") %}
 3
 4<head>
 5    <meta charset="UTF-8">
 6    <title>Directory listing for /{{ path }}</title>
 7</head>
 8
 9<body>
10    <h1>Directory listing for /{{ path }}</h1>
11
12    <input type="text" placeholder="Search...">
13
14    <ul>
15        {# Going to parent directory if not alredy in #}
16        {% if path %}
17            <li><a href="?path=/{{ "".join(path.split('/')[:-1]) }}">..</a></li>
18        {% endif %}
19
20        {# Listing items #}
21        {% for item in items %}
22            <li><a href="?path={{ f'/{path}/{item}' if path else f'/{item}' }}">{{ item }}</a></li>
23        {% endfor %}
24
25    </ul>
26
27    {# Script for filtering items #}
28    <script>
29        const search = document.querySelector('input');
30        const items = document.querySelectorAll('li');
31
32        search.addEventListener('keyup', (e) => {
33            const term = e.target.value.toLowerCase();
34
35            items.forEach(item => {
36                const text = item.innerText.toLowerCase();
37
38                if (text.indexOf(term) != -1) {
39                    item.style.display = 'list-item';
40                } else {
41                    item.style.display = 'none';
42                }
43            });
44        });
45    </script>
46</body>
47
48</html>
examples/httpserver_templates.py
 1# SPDX-FileCopyrightText: 2023 Michal Pokusa
 2#
 3# SPDX-License-Identifier: Unlicense
 4import os
 5import re
 6
 7import socketpool
 8import wifi
 9
10from adafruit_httpserver import Server, Request, Response, FileResponse
11
12try:
13    from adafruit_templateengine import render_template
14except ImportError as e:
15    raise ImportError("This example requires adafruit_templateengine library.") from e
16
17
18pool = socketpool.SocketPool(wifi.radio)
19server = Server(pool, "/static", debug=True)
20
21# Create /static directory if it doesn't exist
22try:
23    os.listdir("/static")
24except OSError as e:
25    raise OSError("Please create a /static directory on the CIRCUITPY drive.") from e
26
27
28def is_file(path: str):
29    return (os.stat(path.rstrip("/"))[0] & 0b_11110000_00000000) == 0b_10000000_00000000
30
31
32@server.route("/")
33def directory_listing(request: Request):
34    path = request.query_params.get("path", "").replace("%20", " ")
35
36    # Preventing path traversal by removing all ../ from path
37    path = re.sub(r"\/(\.\.)\/|\/(\.\.)|(\.\.)\/", "/", path).strip("/")
38
39    # If path is a file, return it as a file response
40    if is_file(f"/static/{path}"):
41        return FileResponse(request, path)
42
43    items = sorted(
44        [
45            item + ("" if is_file(f"/static/{path}/{item}") else "/")
46            for item in os.listdir(f"/static/{path}")
47        ],
48        key=lambda item: not item.endswith("/"),
49    )
50
51    # Otherwise, return a directory listing
52    return Response(
53        request,
54        render_template(
55            "directory_listing.tpl.html",
56            context={"path": path, "items": items},
57        ),
58        content_type="text/html",
59    )
60
61
62# Start the server.
63server.serve_forever(str(wifi.radio.ipv4_address))

Form data parsing

Another way to pass data to the handler function is to use form data. Remember that it is only possible to use it with POST method. More about POST method.

It is important to use correct enctype, depending on the type of data you want to send.

  • application/x-www-form-urlencoded - For sending simple text data without any special characters including spaces.

    If you use it, values will be automatically parsed as strings, but special characters will be URL encoded e.g. "Hello World! ^-$%" will be saved as "Hello+World%21+%5E-%24%25"

  • multipart/form-data - For sending textwith special characters and files

    When used, non-file values will be automatically parsed as strings and non plain text files will be saved as bytes. e.g. "Hello World! ^-$%" will be saved as 'Hello World! ^-$%', and e.g. a PNG file will be saved as b'\x89PNG\r\n\x1a\n\x00\....

  • text/plain - For sending text data with special characters.

    If used, values will be automatically parsed as strings, including special characters, emojis etc. e.g. "Hello World! ^-$%" will be saved as "Hello World! ^-$%", this is the recommended option.

If you pass multiple values with the same name, they will be saved as a list, that can be accessed using request.form_data.get_list(). Even if there is only one value, it will still get a list, and if there multiple values, but you use request.form_data.get() it will return only the first one.

examples/httpserver_form_data.py
 1# SPDX-FileCopyrightText: 2023 Michał Pokusa
 2#
 3# SPDX-License-Identifier: Unlicense
 4
 5import socketpool
 6import wifi
 7
 8from adafruit_httpserver import Server, Request, Response, GET, POST
 9
10
11pool = socketpool.SocketPool(wifi.radio)
12server = Server(pool, debug=True)
13
14
15FORM_HTML_TEMPLATE = """
16<html lang="en">
17    <head>
18        <title>Form with {enctype} enctype</title>
19    </head>
20    <body>
21        <a href="/form?enctype=application/x-www-form-urlencoded">
22            <button>Load <strong>application/x-www-form-urlencoded</strong> form</button>
23        </a><br />
24        <a href="/form?enctype=multipart/form-data">
25            <button>Load <strong>multipart/form-data</strong> form</button>
26        </a><br />
27        <a href="/form?enctype=text/plain">
28            <button>Load <strong>text/plain</strong> form</button>
29        </a><br />
30
31        <h2>Form with {enctype} enctype</h2>
32        <form action="/form" method="post" enctype="{enctype}">
33            <input type="text" name="something" placeholder="Type something...">
34            <input type="submit" value="Submit">
35        </form>
36        {submitted_value}
37    </body>
38</html>
39"""
40
41
42@server.route("/form", [GET, POST])
43def form(request: Request):
44    """
45    Serve a form with the given enctype, and display back the submitted value.
46    """
47    enctype = request.query_params.get("enctype", "text/plain")
48
49    if request.method == POST:
50        posted_value = request.form_data.get("something")
51
52    return Response(
53        request,
54        FORM_HTML_TEMPLATE.format(
55            enctype=enctype,
56            submitted_value=(
57                f"<h3>Enctype: {enctype}</h3>\n<h3>Submitted form value: {posted_value}</h3>"
58                if request.method == POST
59                else ""
60            ),
61        ),
62        content_type="text/html",
63    )
64
65
66server.serve_forever(str(wifi.radio.ipv4_address))

Cookies

You can use cookies to store data on the client side, that will be sent back to the server with every request. They are often used to store authentication tokens, session IDs, but also user preferences e.g. theme.

To access cookies, use request.cookies dictionary. In order to set cookies, pass cookies dictionary to Response constructor or manually add Set-Cookie header.

examples/httpserver_cookies.py
 1# SPDX-FileCopyrightText: 2023 Michał Pokusa
 2#
 3# SPDX-License-Identifier: Unlicense
 4
 5import socketpool
 6import wifi
 7
 8from adafruit_httpserver import Server, Request, Response, GET, Headers
 9
10
11pool = socketpool.SocketPool(wifi.radio)
12server = Server(pool, debug=True)
13
14
15THEMES = {
16    "dark": {
17        "background-color": "#1c1c1c",
18        "color": "white",
19        "button-color": "#181818",
20    },
21    "light": {
22        "background-color": "white",
23        "color": "#1c1c1c",
24        "button-color": "white",
25    },
26}
27
28
29def themed_template(user_preferred_theme: str):
30    theme = THEMES[user_preferred_theme]
31
32    return f"""
33    <html>
34        <head>
35            <title>Cookie Example</title>
36            <style>
37                body {{
38                    background-color: {theme['background-color']};
39                    color: {theme['color']};
40                }}
41
42                button {{
43                    background-color: {theme['button-color']};
44                    color: {theme['color']};
45                    border: 1px solid {theme['color']};
46                    padding: 10px;
47                    margin: 10px;
48                }}
49            </style>
50        </head>
51        <body>
52            <a href="/?theme=dark"><button>Dark theme</button></a>
53            <a href="/?theme=light"><button>Light theme</button></a>
54            <br />
55            <p>
56                After changing the theme, close the tab and open again.
57                Notice that theme stays the same.
58            </p>
59        </body>
60    </html>
61    """
62
63
64@server.route("/", GET)
65def themed_from_cookie(request: Request):
66    """
67    Serve a simple themed page, based on the user's cookie.
68    """
69
70    user_theme = request.cookies.get("theme", "light")
71    wanted_theme = request.query_params.get("theme", user_theme)
72
73    headers = Headers()
74    headers.add("Set-Cookie", "cookie1=value1")
75    headers.add("Set-Cookie", "cookie2=value2")
76
77    return Response(
78        request,
79        themed_template(wanted_theme),
80        content_type="text/html",
81        headers=headers,
82        cookies={} if user_theme == wanted_theme else {"theme": wanted_theme},
83    )
84
85
86server.serve_forever(str(wifi.radio.ipv4_address))

Chunked response

Library supports chunked responses. This is useful for streaming large amounts of data. In order to use it, you need pass a generator that yields chunks of data to a ChunkedResponse constructor.

examples/httpserver_chunked.py
 1# SPDX-FileCopyrightText: 2022 Michał Pokusa
 2#
 3# SPDX-License-Identifier: Unlicense
 4
 5import socketpool
 6import wifi
 7
 8from adafruit_httpserver import Server, Request, ChunkedResponse
 9
10
11pool = socketpool.SocketPool(wifi.radio)
12server = Server(pool, debug=True)
13
14
15@server.route("/chunked")
16def chunked(request: Request):
17    """
18    Return the response with ``Transfer-Encoding: chunked``.
19    """
20
21    def body():
22        yield "Adaf"
23        yield b"ruit"  # Data chunk can be bytes or str.
24        yield " Indus"
25        yield b"tr"
26        yield "ies"
27
28    return ChunkedResponse(request, body)
29
30
31server.serve_forever(str(wifi.radio.ipv4_address))

URL parameters and wildcards

Alternatively to using query parameters, you can use URL parameters. They are a better choice when you want to perform different actions based on the URL. Query/GET parameters are better suited for modifying the behaviour of the handler function.

Of course it is only a suggestion, you can use them interchangeably and/or both at the same time.

In order to use URL parameters, you need to wrap them inside with angle brackets in Server.route, e.g. <my_parameter>.

All URL parameters values are passed as keyword arguments to the handler function.

Notice how the handler function in example below accepts two additional arguments : device_id and action.

If you specify multiple routes for single handler function and they have different number of URL parameters, make sure to add default values for all the ones that might not be passed. In the example below the second route has only one URL parameter, so the action parameter has a default value.

Keep in mind that URL parameters are always passed as strings, so you need to convert them to the desired type. Also note that the names of the function parameters have to match with the ones used in route, but they do not have to be in the same order.

Alternatively you can use e.g. **params to get all the parameters as a dictionary and access them using params['parameter_name'].

It is also possible to specify a wildcard route:

  • ... - matches one path segment, e.g /api/... will match /api/123, but not /api/123/456

  • .... - matches multiple path segments, e.g /api/.... will match /api/123 and /api/123/456

In both cases, wildcards will not match empty path segment, so /api/.../users will match /api/v1/users, but not /api//users or /api/users.

examples/httpserver_url_parameters.py
 1# SPDX-FileCopyrightText: 2023 Michał Pokusa
 2#
 3# SPDX-License-Identifier: Unlicense
 4
 5import socketpool
 6import wifi
 7
 8from adafruit_httpserver import Server, Request, Response
 9
10
11pool = socketpool.SocketPool(wifi.radio)
12server = Server(pool, debug=True)
13
14
15class Device:
16    def turn_on(self):  # pylint: disable=no-self-use
17        print("Turning on device.")
18
19    def turn_off(self):  # pylint: disable=no-self-use
20        print("Turning off device.")
21
22
23def get_device(device_id: str) -> Device:  # pylint: disable=unused-argument
24    """
25    This is a **made up** function that returns a `Device` object.
26    """
27    return Device()
28
29
30@server.route("/device/<device_id>/action/<action>")
31@server.route("/device/emergency-power-off/<device_id>")
32def perform_action(
33    request: Request, device_id: str, action: str = "emergency_power_off"
34):
35    """
36    Performs an "action" on a specified device.
37    """
38
39    device = get_device(device_id)
40
41    if action in ["turn_on"]:
42        device.turn_on()
43    elif action in ["turn_off", "emergency_power_off"]:
44        device.turn_off()
45    else:
46        return Response(request, f"Unknown action ({action})")
47
48    return Response(
49        request, f"Action ({action}) performed on device with ID: {device_id}"
50    )
51
52
53@server.route("/device/<device_id>/status/<date>")
54def device_status_on_date(request: Request, **params: dict):
55    """
56    Return the status of a specified device between two dates.
57    """
58
59    device_id = params.get("device_id")
60    date = params.get("date")
61
62    return Response(request, f"Status of {device_id} on {date}: ...")
63
64
65@server.route("/device/.../status", append_slash=True)
66@server.route("/device/....", append_slash=True)
67def device_status(request: Request):
68    """
69    Returns the status of all devices no matter what their ID is.
70    Unknown commands also return the status of all devices.
71    """
72
73    return Response(request, "Status of all devices: ...")
74
75
76server.serve_forever(str(wifi.radio.ipv4_address))

Authentication

In order to increase security of your server, you can use Basic and Bearer authentication. Remember that it is not a replacement for HTTPS, traffic is still sent in plain text, but it can be used to protect your server from unauthorized access.

If you want to apply authentication to the whole server, you need to call .require_authentication on Server instance.

examples/httpserver_authentication_server.py
 1# SPDX-FileCopyrightText: 2023 Michał Pokusa
 2#
 3# SPDX-License-Identifier: Unlicense
 4
 5import socketpool
 6import wifi
 7
 8from adafruit_httpserver import Server, Request, Response, Basic, Token, Bearer
 9
10
11# Create a list of available authentication methods.
12auths = [
13    Basic("user", "password"),
14    Token("2db53340-4f9c-4f70-9037-d25bee77eca6"),
15    Bearer("642ec696-2a79-4d60-be3a-7c9a3164d766"),
16]
17
18pool = socketpool.SocketPool(wifi.radio)
19server = Server(pool, "/static", debug=True)
20server.require_authentication(auths)
21
22
23@server.route("/implicit-require")
24def implicit_require_authentication(request: Request):
25    """
26    Implicitly require authentication because of the server.require_authentication() call.
27    """
28
29    return Response(request, body="Authenticated", content_type="text/plain")
30
31
32server.serve_forever(str(wifi.radio.ipv4_address))

On the other hand, if you want to apply authentication to a set of routes, you need to call require_authentication function. In both cases you can check if request is authenticated by calling check_authentication on it.

examples/httpserver_authentication_handlers.py
 1# SPDX-FileCopyrightText: 2023 Michał Pokusa
 2#
 3# SPDX-License-Identifier: Unlicense
 4
 5import socketpool
 6import wifi
 7
 8from adafruit_httpserver import Server, Request, Response, UNAUTHORIZED_401
 9from adafruit_httpserver.authentication import (
10    AuthenticationError,
11    Basic,
12    Token,
13    Bearer,
14    check_authentication,
15    require_authentication,
16)
17
18
19pool = socketpool.SocketPool(wifi.radio)
20server = Server(pool, debug=True)
21
22# Create a list of available authentication methods.
23auths = [
24    Basic("user", "password"),
25    Token("2db53340-4f9c-4f70-9037-d25bee77eca6"),
26    Bearer("642ec696-2a79-4d60-be3a-7c9a3164d766"),
27]
28
29
30@server.route("/check")
31def check_if_authenticated(request: Request):
32    """
33    Check if the request is authenticated and return a appropriate response.
34    """
35    is_authenticated = check_authentication(request, auths)
36
37    return Response(
38        request,
39        body="Authenticated" if is_authenticated else "Not authenticated",
40        content_type="text/plain",
41    )
42
43
44@server.route("/require-or-401")
45def require_authentication_or_401(request: Request):
46    """
47    Require authentication and return a default server 401 response if not authenticated.
48    """
49    require_authentication(request, auths)
50
51    return Response(request, body="Authenticated", content_type="text/plain")
52
53
54@server.route("/require-or-handle")
55def require_authentication_or_manually_handle(request: Request):
56    """
57    Require authentication and manually handle request if not authenticated.
58    """
59
60    try:
61        require_authentication(request, auths)
62
63        return Response(request, body="Authenticated", content_type="text/plain")
64
65    except AuthenticationError:
66        return Response(
67            request,
68            body="Not authenticated - Manually handled",
69            content_type="text/plain",
70            status=UNAUTHORIZED_401,
71        )
72
73
74server.serve_forever(str(wifi.radio.ipv4_address))

Redirects

Sometimes you might want to redirect the user to a different URL, either on the same server or on a different one.

You can do that by returning Redirect from your handler function.

You can specify wheter the redirect is permanent or temporary by passing permanent=... to Redirect. If you need the redirect to preserve the original request method, you can set preserve_method=True.

Alternatively, you can pass a status object directly to Redirect constructor.

examples/httpserver_redirects.py
 1# SPDX-FileCopyrightText: 2023 Michał Pokusa
 2#
 3# SPDX-License-Identifier: Unlicense
 4
 5import socketpool
 6import wifi
 7
 8from adafruit_httpserver import (
 9    Server,
10    Request,
11    Response,
12    Redirect,
13    POST,
14    NOT_FOUND_404,
15    MOVED_PERMANENTLY_301,
16)
17
18
19pool = socketpool.SocketPool(wifi.radio)
20server = Server(pool, debug=True)
21
22REDIRECTS = {
23    "google": "https://www.google.com",
24    "adafruit": "https://www.adafruit.com",
25    "circuitpython": "https://circuitpython.org",
26}
27
28
29@server.route("/blinka")
30def redirect_blinka(request: Request):
31    """Always redirect to a Blinka page as permanent redirect."""
32    return Redirect(request, "https://circuitpython.org/blinka", permanent=True)
33
34
35@server.route("/adafruit")
36def redirect_adafruit(request: Request):
37    """Permanent redirect to Adafruit website with explicitly set status code."""
38    return Redirect(request, "https://www.adafruit.com/", status=MOVED_PERMANENTLY_301)
39
40
41@server.route("/fake-login", POST)
42def fake_login(request: Request):
43    """Fake login page."""
44    return Response(request, "Fake login page with POST data preserved.")
45
46
47@server.route("/login", POST)
48def temporary_login_redirect(request: Request):
49    """Temporary moved login page with preserved POST data."""
50    return Redirect(request, "/fake-login", preserve_method=True)
51
52
53@server.route("/<slug>")
54def redirect_other(request: Request, slug: str = None):
55    """
56    Redirect to a URL based on the slug.
57    """
58
59    if slug is None or slug not in REDIRECTS:
60        return Response(request, "Unknown redirect", status=NOT_FOUND_404)
61
62    return Redirect(request, REDIRECTS.get(slug))
63
64
65server.serve_forever(str(wifi.radio.ipv4_address))

Server-Sent Events

All types of responses until now were synchronous, meaning that the response was sent immediately after the handler function returned. However, sometimes you might want to send data to the client at a later time, e.g. when some event occurs. This can be overcomed by periodically polling the server, but it is not an elegant solution. Instead, you can use Server-Sent Events (SSE).

Response is initialized on return, events can be sent using .send_event() method. Due to the nature of SSE, it is necessary to store the response object somewhere, so that it can be accessed later.

Because of the limited number of concurrently open sockets, it is not possible to process more than one SSE response at the same time. This might change in the future, but for now, it is recommended to use SSE only with one client at a time.

examples/httpserver_sse.py
 1# SPDX-FileCopyrightText: 2023 Michał Pokusa
 2#
 3# SPDX-License-Identifier: Unlicense
 4
 5from time import monotonic
 6import microcontroller
 7import socketpool
 8import wifi
 9
10from adafruit_httpserver import Server, Request, Response, SSEResponse, GET
11
12
13pool = socketpool.SocketPool(wifi.radio)
14server = Server(pool, debug=True)
15
16
17sse_response: SSEResponse = None
18next_event_time = monotonic()
19
20HTML_TEMPLATE = """
21<html lang="en">
22    <head>
23        <title>Server-Sent Events Client</title>
24    </head>
25    <body>
26        <p>CPU temperature: <strong>-</strong>&deg;C</p>
27        <script>
28            const eventSource = new EventSource('/connect-client');
29            const cpuTemp = document.querySelector('strong');
30
31            eventSource.onmessage = event => cpuTemp.textContent = event.data;
32            eventSource.onerror = error => cpuTemp.textContent = error;
33        </script>
34    </body>
35</html>
36"""
37
38
39@server.route("/client", GET)
40def client(request: Request):
41    return Response(request, HTML_TEMPLATE, content_type="text/html")
42
43
44@server.route("/connect-client", GET)
45def connect_client(request: Request):
46    global sse_response  # pylint: disable=global-statement
47
48    if sse_response is not None:
49        sse_response.close()  # Close any existing connection
50
51    sse_response = SSEResponse(request)
52
53    return sse_response
54
55
56server.start(str(wifi.radio.ipv4_address))
57while True:
58    server.poll()
59
60    # Send an event every second
61    if sse_response is not None and next_event_time < monotonic():
62        cpu_temp = round(microcontroller.cpu.temperature, 2)
63        sse_response.send_event(str(cpu_temp))
64        next_event_time = monotonic() + 1

Websockets

Although SSE provide a simple way to send data from the server to the client, they are not suitable for sending data the other way around.

For that purpose, you can use Websockets. They are more complex than SSE, but they provide a persistent two-way communication channel between the client and the server.

Remember, that because Websockets also receive data, you have to explicitly call .receive() on the Websocket object to get the message. This is anologous to calling .poll() on the Server object.

The following example uses asyncio, which has to be installed separately. It is not necessary to use asyncio to use Websockets, but it is recommended as it makes it easier to handle multiple tasks. It can be used in any of the examples, but here it is particularly useful.

Because of the limited number of concurrently open sockets, it is not possible to process more than one Websocket response at the same time. This might change in the future, but for now, it is recommended to use Websocket only with one client at a time.

examples/httpserver_websocket.py
  1# SPDX-FileCopyrightText: 2023 Michał Pokusa
  2#
  3# SPDX-License-Identifier: Unlicense
  4
  5from asyncio import create_task, gather, run, sleep as async_sleep
  6import board
  7import microcontroller
  8import neopixel
  9import socketpool
 10import wifi
 11
 12from adafruit_httpserver import Server, Request, Response, Websocket, GET
 13
 14
 15pool = socketpool.SocketPool(wifi.radio)
 16server = Server(pool, debug=True)
 17
 18pixel = neopixel.NeoPixel(board.NEOPIXEL, 1)
 19
 20websocket: Websocket = None
 21
 22HTML_TEMPLATE = """
 23<html lang="en">
 24    <head>
 25        <title>Websocket Client</title>
 26    </head>
 27    <body>
 28        <p>CPU temperature: <strong>-</strong>&deg;C</p>
 29        <p>NeoPixel Color: <input type="color"></p>
 30        <script>
 31            const cpuTemp = document.querySelector('strong');
 32            const colorPicker = document.querySelector('input[type="color"]');
 33
 34            let ws = new WebSocket('ws://' + location.host + '/connect-websocket');
 35
 36            ws.onopen = () => console.log('WebSocket connection opened');
 37            ws.onclose = () => console.log('WebSocket connection closed');
 38            ws.onmessage = event => cpuTemp.textContent = event.data;
 39            ws.onerror = error => cpuTemp.textContent = error;
 40
 41            colorPicker.oninput = debounce(() => ws.send(colorPicker.value), 200);
 42
 43            function debounce(callback, delay = 1000) {
 44                let timeout
 45                return (...args) => {
 46                    clearTimeout(timeout)
 47                    timeout = setTimeout(() => {
 48                    callback(...args)
 49                  }, delay)
 50                }
 51            }
 52        </script>
 53    </body>
 54</html>
 55"""
 56
 57
 58@server.route("/client", GET)
 59def client(request: Request):
 60    return Response(request, HTML_TEMPLATE, content_type="text/html")
 61
 62
 63@server.route("/connect-websocket", GET)
 64def connect_client(request: Request):
 65    global websocket  # pylint: disable=global-statement
 66
 67    if websocket is not None:
 68        websocket.close()  # Close any existing connection
 69
 70    websocket = Websocket(request)
 71
 72    return websocket
 73
 74
 75server.start(str(wifi.radio.ipv4_address))
 76
 77
 78async def handle_http_requests():
 79    while True:
 80        server.poll()
 81
 82        await async_sleep(0)
 83
 84
 85async def handle_websocket_requests():
 86    while True:
 87        if websocket is not None:
 88            if (data := websocket.receive(fail_silently=True)) is not None:
 89                r, g, b = int(data[1:3], 16), int(data[3:5], 16), int(data[5:7], 16)
 90                pixel.fill((r, g, b))
 91
 92        await async_sleep(0)
 93
 94
 95async def send_websocket_messages():
 96    while True:
 97        if websocket is not None:
 98            cpu_temp = round(microcontroller.cpu.temperature, 2)
 99            websocket.send_message(str(cpu_temp), fail_silently=True)
100
101        await async_sleep(1)
102
103
104async def main():
105    await gather(
106        create_task(handle_http_requests()),
107        create_task(handle_websocket_requests()),
108        create_task(send_websocket_messages()),
109    )
110
111
112run(main())

Multiple servers

Although it is not the primary use case, it is possible to run multiple servers at the same time. In order to do that, you need to create multiple Server instances and call .start() and .poll() on each of them. Using .serve_forever() for this is not possible because of it’s blocking behaviour.

Each server must have a different port number.

In order to distinguish between responses from different servers a ‘X-Server’ header is added to each response. This is an optional step, both servers will work without it.

In combination with separate authentication and diffrent root_path this allows creating moderately complex setups. You can share same handler functions between servers or use different ones for each server.

examples/httpserver_multiple_servers.py
 1# SPDX-FileCopyrightText: 2023 Michał Pokusa
 2#
 3# SPDX-License-Identifier: Unlicense
 4
 5import socketpool
 6import wifi
 7
 8from adafruit_httpserver import Server, Request, Response
 9
10
11pool = socketpool.SocketPool(wifi.radio)
12
13bedroom_server = Server(pool, "/bedroom", debug=True)
14bedroom_server.headers["X-Server"] = "Bedroom"
15
16office_server = Server(pool, "/office", debug=True)
17office_server.headers["X-Server"] = "Office"
18
19
20@bedroom_server.route("/bedroom")
21def bedroom(request: Request):
22    """
23    This route is registered only on ``bedroom_server``.
24    """
25    return Response(request, "Hello from the bedroom!")
26
27
28@office_server.route("/office")
29def office(request: Request):
30    """
31    This route is registered only on ``office_server``.
32    """
33    return Response(request, "Hello from the office!")
34
35
36@bedroom_server.route("/home")
37@office_server.route("/home")
38def home(request: Request):
39    """
40    This route is registered on both servers.
41    """
42    return Response(request, "Hello from home!")
43
44
45id_address = str(wifi.radio.ipv4_address)
46
47# Start the servers.
48bedroom_server.start(id_address, 5000)
49office_server.start(id_address, 8000)
50
51while True:
52    try:
53        # Process any waiting requests for both servers.
54        bedroom_server.poll()
55        office_server.poll()
56    except OSError as error:
57        print(error)
58        continue

Debug mode

It is highly recommended to disable debug mode in production.

During development it is useful to see the logs from the server. You can enable debug mode by setting debug=True on Server instance or in constructor, it is disabled by default.

Debug mode prints messages on server startup, after sending a response to a request and if exception occurs during handling of the request in .serve_forever().

This is how the logs might look like when debug mode is enabled:

Started development server on http://192.168.0.100:5000
192.168.0.101 -- "GET /" 194 -- "200 OK" 154 -- 96ms
192.168.0.101 -- "GET /example" 134 -- "404 Not Found" 172 -- 123ms
192.168.0.102 -- "POST /api" 1241 -- "401 Unauthorized" 95 -- 64ms
Traceback (most recent call last):
    ...
    File "code.py", line 55, in example_handler
KeyError: non_existent_key
192.168.0.103 -- "GET /index.html" 242 -- "200 OK" 154 -- 182ms
Stopped development server

This is the default format of the logs:

{client_ip} -- "{request_method} {path}" {request_size} -- "{response_status}" {response_size} -- {elapsed_ms}

If you need more information about the server or request, or you want it in a different format you can modify functions at the bottom of adafruit_httpserver/server.py that start with _debug_....

NOTE: This is an advanced usage that might change in the future. It is not recommended to modify other parts of the code.