Simple Test
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.
This is the minimal example of using the library. This example is serving a simple static text message.
It also manually connects to the WiFi network.
1# SPDX-FileCopyrightText: 2022 Dan Halbert for Adafruit Industries
2#
3# SPDX-License-Identifier: Unlicense
4
5import os
6
7import socketpool
8import wifi
9
10from adafruit_httpserver import Server, Request, Response
11
12ssid = os.getenv("WIFI_SSID")
13password = os.getenv("WIFI_PASSWORD")
14
15print("Connecting to", ssid)
16wifi.radio.connect(ssid, password)
17print("Connected to", ssid)
18
19pool = socketpool.SocketPool(wifi.radio)
20server = Server(pool, "/static", debug=True)
21
22
23@server.route("/")
24def base(request: Request):
25 """
26 Serve a default static plain text message.
27 """
28 return Response(request, "Hello from the CircuitPython HTTP Server!")
29
30
31server.serve_forever(str(wifi.radio.ipv4_address))
Although there is nothing wrong with this approach, from the version 8.0.0 of CircuitPython,
it is possible to use the environment variables
defined in settings.toml
file to store secrets and configure the WiFi network.
This is the same example as above, but it uses the settings.toml
file to configure the WiFi network.
From now on, all the examples will use the settings.toml
file to configure the WiFi network.
1# Setting these variables will automatically connect board to WiFi on boot
2CIRCUITPY_WIFI_SSID="Your WiFi SSID Here"
3CIRCUITPY_WIFI_PASSWORD="Your WiFi Password Here"
Note that we still need to import socketpool
and wifi
modules.
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 Server, Request, Response
9
10
11pool = socketpool.SocketPool(wifi.radio)
12server = Server(pool, "/static", debug=True)
13
14
15@server.route("/")
16def base(request: Request):
17 """
18 Serve a default static plain text message.
19 """
20 return Response(request, "Hello from the CircuitPython HTTP Server!")
21
22
23server.serve_forever(str(wifi.radio.ipv4_address))
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.
1# SPDX-FileCopyrightText: 2022 Dan Halbert for Adafruit Industries
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, but you can change it.
1# SPDX-FileCopyrightText: 2022 Dan Halbert for Adafruit Industries
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, "/static", debug=True)
14
15
16@server.route("/home")
17def home(request: Request):
18 """
19 Serves the file /www/home.html.
20 """
21
22 return FileResponse(request, "home.html", "/www")
23
24
25server.serve_forever(str(wifi.radio.ipv4_address))
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.
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 Server, Request, FileResponse
9
10
11pool = socketpool.SocketPool(wifi.radio)
12server = Server(pool, "/static", debug=True)
13
14
15@server.route("/")
16def base(request: Request):
17 """
18 Serve the default index.html file.
19 """
20 return FileResponse(request, "index.html")
21
22
23# Start the server.
24server.start(str(wifi.radio.ipv4_address))
25
26while True:
27 try:
28 # Do something useful in this section,
29 # for example read a sensor and capture an average,
30 # or a running total of the last 10 samples
31
32 # Process any waiting requests
33 server.poll()
34
35 # If you want you can stop the server by calling server.stop() anywhere in your code
36 except OSError as error:
37 print(error)
38 continue
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 http://custom-mdns-hostname/
and http://custom-mdns-hostname.local/
.
1# SPDX-FileCopyrightText: 2022 Dan Halbert for Adafruit Industries
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=80)
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.
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
11pool = socketpool.SocketPool(wifi.radio)
12server = Server(pool, debug=True)
13
14
15@server.route("/cpu-information", append_slash=True)
16def cpu_information_handler(request: Request):
17 """
18 Return the current CPU temperature, frequency, and voltage as JSON.
19 """
20
21 data = {
22 "temperature": microcontroller.cpu.temperature,
23 "frequency": microcontroller.cpu.frequency,
24 "voltage": microcontroller.cpu.voltage,
25 }
26
27 return JSONResponse(request, data)
28
29
30server.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.
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 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 userequest.json()
to parse it into a dictionaryAlternatively 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.
1# SPDX-FileCopyrightText: 2022 Dan Halbert for Adafruit Industries
2#
3# SPDX-License-Identifier: Unlicense
4
5import board
6import neopixel
7import socketpool
8import wifi
9
10from adafruit_httpserver import Server, 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@server.route("/change-neopixel-color", GET)
20def change_neopixel_color_handler_query_params(request: Request):
21 """Changes the color of the built-in NeoPixel using query/GET params."""
22
23 # e.g. /change-neopixel-color?r=255&g=0&b=0
24
25 r = request.query_params.get("r") or 0
26 g = request.query_params.get("g") or 0
27 b = request.query_params.get("b") or 0
28
29 pixel.fill((int(r), int(g), int(b)))
30
31 return Response(request, f"Changed NeoPixel to color ({r}, {g}, {b})")
32
33
34@server.route("/change-neopixel-color", POST)
35def change_neopixel_color_handler_post_body(request: Request):
36 """Changes the color of the built-in NeoPixel using POST body."""
37
38 data = request.body # e.g b"255,0,0"
39 r, g, b = data.decode().split(",") # ["255", "0", "0"]
40
41 pixel.fill((int(r), int(g), int(b)))
42
43 return Response(request, f"Changed NeoPixel to color ({r}, {g}, {b})")
44
45
46@server.route("/change-neopixel-color/json", POST)
47def change_neopixel_color_handler_post_json(request: Request):
48 """Changes the color of the built-in NeoPixel using JSON POST body."""
49
50 data = request.json() # e.g {"r": 255, "g": 0, "b": 0}
51 r, g, b = data.get("r", 0), data.get("g", 0), data.get("b", 0)
52
53 pixel.fill((r, g, b))
54
55 return Response(request, f"Changed NeoPixel to color ({r}, {g}, {b})")
56
57
58@server.route("/change-neopixel-color/<r>/<g>/<b>", GET)
59def change_neopixel_color_handler_url_params(
60 request: Request, r: str = "0", g: str = "0", b: str = "0"
61):
62 """Changes the color of the built-in NeoPixel using URL params."""
63
64 # e.g. /change-neopixel-color/255/0/0
65
66 pixel.fill((int(r), int(g), int(b)))
67
68 return Response(request, f"Changed NeoPixel to color ({r}, {g}, {b})")
69
70
71server.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.
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 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.
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
.
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 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/.../status", append_slash=True)
54@server.route("/device/....", append_slash=True)
55def device_status(request: Request):
56 """
57 Returns the status of all devices no matter what their ID is.
58 Unknown commands also return the status of all devices.
59 """
60
61 return Response(request, "Status of all devices: ...")
62
63
64server.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.
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 Server, Request, Response, Basic, Bearer
9
10
11# Create a list of available authentication methods.
12auths = [
13 Basic("user", "password"),
14 Bearer("642ec696-2a79-4d60-be3a-7c9a3164d766"),
15]
16
17pool = socketpool.SocketPool(wifi.radio)
18server = Server(pool, "/static", debug=True)
19server.require_authentication(auths)
20
21
22@server.route("/implicit-require")
23def implicit_require_authentication(request: Request):
24 """
25 Implicitly require authentication because of the server.require_authentication() call.
26 """
27
28 return Response(request, body="Authenticated", content_type="text/plain")
29
30
31server.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.
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 Server, Request, Response, UNATUHORIZED_401
9from adafruit_httpserver.authentication import (
10 AuthenticationError,
11 Basic,
12 Bearer,
13 check_authentication,
14 require_authentication,
15)
16
17
18pool = socketpool.SocketPool(wifi.radio)
19server = Server(pool, debug=True)
20
21# Create a list of available authentication methods.
22auths = [
23 Basic("user", "password"),
24 Bearer("642ec696-2a79-4d60-be3a-7c9a3164d766"),
25]
26
27
28@server.route("/check")
29def check_if_authenticated(request: Request):
30 """
31 Check if the request is authenticated and return a appropriate response.
32 """
33 is_authenticated = check_authentication(request, auths)
34
35 return Response(
36 request,
37 body="Authenticated" if is_authenticated else "Not authenticated",
38 content_type="text/plain",
39 )
40
41
42@server.route("/require-or-401")
43def require_authentication_or_401(request: Request):
44 """
45 Require authentication and return a default server 401 response if not authenticated.
46 """
47 require_authentication(request, auths)
48
49 return Response(request, body="Authenticated", content_type="text/plain")
50
51
52@server.route("/require-or-handle")
53def require_authentication_or_manually_handle(request: Request):
54 """
55 Require authentication and manually handle request if not authenticated.
56 """
57
58 try:
59 require_authentication(request, auths)
60
61 return Response(request, body="Authenticated", content_type="text/plain")
62
63 except AuthenticationError:
64 return Response(
65 request,
66 body="Not authenticated - Manually handled",
67 content_type="text/plain",
68 status=UNATUHORIZED_401,
69 )
70
71
72server.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
.
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 Server, Request, Response, Redirect, NOT_FOUND_404
9
10
11pool = socketpool.SocketPool(wifi.radio)
12server = Server(pool, debug=True)
13
14REDIRECTS = {
15 "google": "https://www.google.com",
16 "adafruit": "https://www.adafruit.com",
17 "circuitpython": "https://circuitpython.org",
18}
19
20
21@server.route("/blinka")
22def redirect_blinka(request: Request):
23 """
24 Always redirect to a Blinka page as permanent redirect.
25 """
26 return Redirect(request, "https://circuitpython.org/blinka", permanent=True)
27
28
29@server.route("/<slug>")
30def redirect_other(request: Request, slug: str = None):
31 """
32 Redirect to a URL based on the slug.
33 """
34
35 if slug is None or not slug in REDIRECTS:
36 return Response(request, "Unknown redirect", status=NOT_FOUND_404)
37
38 return Redirect(request, REDIRECTS.get(slug))
39
40
41server.serve_forever(str(wifi.radio.ipv4_address))
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 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.
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 Server, Request, Response
9
10
11pool = socketpool.SocketPool(wifi.radio)
12
13bedroom_server = Server(pool, "/bedroom", debug=True)
14office_server = Server(pool, "/office", debug=True)
15
16
17@bedroom_server.route("/bedroom")
18def bedroom(request: Request):
19 """
20 This route is registered only on ``bedroom_server``.
21 """
22 return Response(request, "Hello from the bedroom!")
23
24
25@office_server.route("/office")
26def office(request: Request):
27 """
28 This route is registered only on ``office_server``.
29 """
30 return Response(request, "Hello from the office!")
31
32
33@bedroom_server.route("/home")
34@office_server.route("/home")
35def home(request: Request):
36 """
37 This route is registered on both servers.
38 """
39 return Response(request, "Hello from home!")
40
41
42id_address = str(wifi.radio.ipv4_address)
43
44# Start the servers.
45bedroom_server.start(id_address, 5000)
46office_server.start(id_address, 8000)
47
48while True:
49 try:
50 # Process any waiting requests for both servers.
51 bedroom_server.poll()
52 office_server.poll()
53 except OSError as error:
54 print(error)
55 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:80
192.168.0.101 -- "GET /" 194 -- "200 OK" 154
192.168.0.101 -- "GET /example" 134 -- "404 Not Found" 172
192.168.0.102 -- "POST /api" 1241 -- "401 Unauthorized" 95
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
Stopped development server
This is the default format of the logs:
{client_ip} -- "{request_method} {path}" {request_size} -- "{response_status}" {response_size}
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.