Giter VIP home page Giter VIP logo

adafruit_circuitpython_httpserver's Introduction

Introduction

Documentation Status

Discord

Build Status

Code Style: Black

HTTP Server for CircuitPython.

  • Supports socketpool or socket as a source of sockets; can be used in CPython.
  • HTTP 1.1.
  • Serves files from a designated root.
  • Routing for serving computed responses from handlers.
  • Gives access to request headers, query parameters, form data, body and client's address (the one from which the request came).
  • Supports chunked transfer encoding.
  • Supports URL parameters and wildcard URLs.
  • Supports HTTP Basic and Bearer Authentication on both server and route per level.
  • Supports Websockets and Server-Sent Events.

Dependencies

This driver depends on:

Please ensure all dependencies are available on the CircuitPython filesystem. This is easily achieved by downloading the Adafruit library and driver bundle or individual libraries can be installed using circup.

Installing from PyPI

On supported GNU/Linux systems like the Raspberry Pi, you can install the driver locally from PyPI. To install for current user:

pip3 install adafruit-circuitpython-httpserver

To install system-wide (this may be required in some cases):

sudo pip3 install adafruit-circuitpython-httpserver

To install in a virtual environment in your current project:

mkdir project-name && cd project-name
python3 -m venv .venv
source .venv/bin/activate
pip3 install adafruit-circuitpython-httpserver

Installing to a Connected CircuitPython Device with Circup

Make sure that you have circup installed in your Python environment. Install it with the following command if necessary:

pip3 install circup

With circup installed and your CircuitPython device connected use the following command to install:

circup install adafruit_httpserver

Or the following command to update an existing version:

circup update

Security

The HTTP server implementation in this package is not robust and should only be deployed on trusted networks. For instance, there are trivial denial of service attacks against adafruit_httpserver. Pull requests that improve the server's security and robustness are of course welcome.

Contributing

Contributions are welcome! Please read our Code of Conduct before contributing to help this project stay welcoming.

Documentation

API documentation for this library can be found on Read the Docs.

For information on building library documentation, please check out this guide.

adafruit_circuitpython_httpserver's People

Contributors

anecdata avatar askpatrickw avatar c01o avatar cthulahoops avatar deshipu avatar dhalbert avatar djdevon3 avatar eirinnm avatar evaherrada avatar foamyguy avatar foxy82 avatar jepler avatar jposada202020 avatar jrrickerson avatar karlfl avatar kattni avatar matemaciek avatar michalpokusa avatar neradoc avatar paul-1 avatar spovlot avatar tedder avatar tekktrik avatar

Stargazers

 avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar

Watchers

 avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar

adafruit_circuitpython_httpserver's Issues

RPI Pico W Circuitpython Webserver on AP

Hello folks (from germany),

danhalbert from adafruits forum guided me here, that's why I'm kindly asking here the original question.

RPI PICO W should:

  • act as an access point
  • provide some routes
  • do something useful in parallel

Following the example https://github.com/adafruit/Adafruit_Ci ... asyncio.py me I hardly tried to spin up the http-server on wifi.radio.ipv4_address_ap rather than on wifi.radio.ipv4_address.
Don't get me wrong: clear decorated routes (@server.route...) are best practice. That's why I welcome that pattern.

This led to no success. Indeed this is what's going on:

  • I can connect to the AP ---> YES (which is 192.168.4.1)
  • the client gets a IP adress assigned ---> YES (which is 192.168.4.XXX)
  • the client can fetch http://192.168.4.1/ --> NO

What's wrong here? Any suggestions are appreciated.

import time
import board
import digitalio
import wifi
import socketpool
from adafruit_httpserver import Server, REQUEST_HANDLED_RESPONSE_SENT, Request, Response
from asyncio import create_task, gather, run, sleep as async_sleep

led = digitalio.DigitalInOut(board.LED)
led.direction = digitalio.Direction.OUTPUT

ap_ssid = "myAP"
ap_password = "12345678"
wifi.radio.start_ap(ssid=ap_ssid, password=ap_password)
print("Access point created with SSID: {}, password: {}".format(ap_ssid, ap_password))
print("My IP address is", wifi.radio.ipv4_address_ap)

pool = socketpool.SocketPool(wifi.radio)
server = Server(pool, "/static", debug=False)
server.start(str(wifi.radio.ipv4_address_ap))

@server.route("/")
def base(request: Request):
    return Response(request, "Hello from the CircuitPython Access Point")

async def handle_http_requests():
    while True:
        pool_result = server.poll()
        if pool_result == REQUEST_HANDLED_RESPONSE_SENT:
            # 
            pass
        await async_sleep(0)

async def do_something_useful():
    global i
    while True:
        led.value = 1
        await async_sleep(1)
        led.value = 0
        await async_sleep(1)

async def main():
    await gather(
        create_task(handle_http_requests()),
        create_task(do_something_useful()),
    )

run(main())

P.S.:

  • "/static" directory exists
  • the client is not browser stuff, I used different HTTP-Clients instead to fetch

socket.accept() is blocking by default

After adding the Start/Poll method I setup some tests to see if other code could run even if there were no requests coming in. Unfortunately, that wasn't the case. Digging into it deeper I discovered that socket.accept() method is blocking by default.

Modifying the example code with a simple print will show the issue.

while True:
    try:
        print("Processing before polling...")
        server.poll()
        print("Processing after polling...")
    except OSError:
       continue

The code will stop at the server.poll() method waiting for a request to come in...

Listening on http://192.168.1.208:80
Processing before polling...

Once a request comes in the other lines will process, then it'll stop at server.poll() again...

Processing after polling...
Processing before polling...

To fix this the socket needs to set blocking to False or it's timeout to 0 in HTTPServer.Start()...

        self._sock.bind((host, port))
        self._sock.listen(5)
        self._sock.setblocking(False) #do not block

There is one caveat to this. When the accept() method doesn't find any incoming connections and blocking is turned off, it raises an exception OSError [Errno 11] EAGAIN. Therefore, server.poll() must be the last thing in the while loop otherwise, the exception will skip any logic after the exception is raised. Which means any logic after server.poll() will only run once a request has been received and processed successfully...

while True:
    try:
        print("Processing before polling...")
        server.poll()
        print("Processing after polling...") #This will never be called if exception is raised.
    except OSError:
       continue

Results in this output...

Listening on http://192.168.1.208:80
Processing before polling...
Processing before polling...
Processing before polling...
Processing before polling...
Received 485 bytes
Processing after polling...
Processing before polling...
Processing before polling...
...  and so on

A possible work around for this would be to handle the specific exception OSError: [Errno 11] EAGAIN in the server.poll() method. But I'm not sure if catching and handling exceptions in library code is an acceptable practice.

no response to http requests on pico w /CP8 Beta

tried circuitpython 8 beta 1/2/3 but the HTTPServer is not usable on pico w.

minimal examples like all three from this very lib: https://github.com/adafruit/Adafruit_CircuitPython_HTTPServer/tree/main/examples
are not repsonding to simple browser or curl requests

ERR_CONNECTION_REFUSED (like about every 50. request does in fact get a response)

then i read this, and he is absolutly right: #14 (comment)
replacing the .mpy to source .py and changing L:334 to
self._sock.setblocking(True)
will make the HTTPServer respond every time

self._sock.setblocking(False) # non-blocking socket

not sure if this is related to this issue: #9 which does belong to this MR: bcabd1d which originally does change this very line to the opposite that makes the lib work for me and @justbuchanan.

sadly this is not a real solution, since it is now waiting/blocking to a incoming request and no other magic can happen while waiting :-(

mdns setting error - trying to set a custom hostname

This code:

...
mdns_server = mdns.Server(wifi.radio)
mdns_server.hostname = "cont"
mdns_server.advertise_service(service_type="_http", protocol="_tcp", port=80)
...

Gives this error:

0;๐Ÿ192.168.178.89 | code.py | 8.2.0-rc.1Traceback (most recent call last):                                                                                                             
  File "code.py", line 29, in <module>                                                                                                                                                  
RuntimeError: Out of MDNS service slots                                                                                                                                                 
0;๐Ÿ192.168.178.89 | [email protected] RuntimeError | 8.2.0-rc.1                                                                                                                               
Code done running.  

Is this because I need write access to my local network's DNS server?
If this worked will it allow me to set a custom hostname different from PicoW?

Setting the hostname - Pico W

The default hostname is PicoW.

How do I set it to something different?

I am using circuit python 8.2
If I do this:

...
from adafruit_httpserver import Server, Request, FileResponse, MIMETypes, GET, JSONResponse
wifi.radio.hostname = str("xx")

...

server.serve_forever(str(wifi.radio.hostname)) # line 67
...

The REPL says:

>>> Wi-Fi: No IP | Done | 8.2.0-rc.1                                                                                                                                                    
Auto-reload is on. Simply save files over USB to run them or enter REPL to disable.                                                                                                     
code.py output:                                                                                                                                                                         
0;๐Ÿ192.168.178.89 | code.py | 8.2.0-rc.1socket_resolve_host() returned -2                                                                                                              
Traceback (most recent call last):                                                                                                                                                      
  File "adafruit_httpserver/server.py", line 138, in _verify_can_start                                                                                                                  
gaierror: (-2, 'Name or service not known')                                                                                                                                             
                                                                                                                                                                                        
The above exception was the direct cause of the following exception:                                                                                                                    
                                                                                                                                                                                        
Traceback (most recent call last):                                                                                                                                                      
  File "code.py", line 67, in <module>                                                                                                                                                  
  File "adafruit_httpserver/server.py", line 151, in serve_forever                                                                                                                      
  File "adafruit_httpserver/server.py", line 170, in start                                                                                                                              
  File "adafruit_httpserver/server.py", line 140, in _verify_can_start                                                                                                                  
RuntimeError: Cannot start server on xx:80                                                                                                                                              
0;๐Ÿ192.168.178.89 | 140@adafruit_httpserver/server.py RuntimeError | 8.2.0-rc.1                                                                                                        
Code done running.                                                                                                                                                                      
0;๐Ÿ192.168.178.89 | REPL | 8.2.0-rc.1                                                                                                                                                  
Adafruit CircuitPython 8.2.0-rc.1 on 2023-06-27; Raspberry Pi Pico W with rp2040

Needless to say removing this and rebooting ...

0;๐ŸWi-Fi: Authentication failure | REPL | 8.2.0-rc.1                                                                                                                                   
Adafruit CircuitPython 8.2.0-rc.1 on 2023-06-27; Raspberry Pi Pico W with rp2040                                                                                                        
>>>                                                                                                                                                                                     
>>> Wi-Fi: No IP | Done | 8.2.0-rc.1                                                                                                                                                    
Auto-reload is on. Simply save files over USB to run them or enter REPL to disable.                                                                                                     
code.py output:                                                                                                                                                                         
0;๐Ÿ192.168.178.89 | code.py | 8.2.0-rc.1Started development server on http://PicoW:80        

Refactor library into class files

The library could benefit from a simple extract of the classes into their own .py files. This will provide for easier maintenance and future expansion of the library. It should be possible to do this in a way that doesn't break existing code.

Something like this...

  1. adafruit_httpserver.py - remove/replace with the following folder
  2. adafruit_httpserver (new folder)
    • __init__.py
    • httprequest.py
    • httpresponse.py
    • httpserver.py
    • httpstatus.py
    • mimetype.py

Submitting empty form, or forms with only checkboxes, causes crash in web server

Hi,

I think I found an issue in the web server included in CircuitPython. If you post a form back to the web server that has no input fields, OR contains only chekboxes and they all are clear (which causes no form data to be posted back), the web server seems to crash.

For example this form:

<form action="/update" method="post">
    <input type="checkbox" name="test" value="1">
    <input type="submit" value="Update">
</form>

When the checkbox is clear, and the form is posted back, when trying to access the field value (or any of the other relevant methods like get(...)):

@server.route("/update", POST)
def update(request: Request):
    x = request.form_data.fields

The web server crashes with this error:

  File "adafruit_httpserver/request.py", line 321, in form_data
  File "adafruit_httpserver/request.py", line 144, in __init__
  File "adafruit_httpserver/request.py", line 157, in _parse_x_www_form_urlencoded
ValueError: need more than 1 values to unpack

This is using adafruit_httpserver from adafruit-circuitpython-bundle-8.x-mpy-20230829.zip on a Rasberry PI Pico on Circuit Python 8.2.4
If I add a text or hidden input in the form for example, it works:

<form action="/update" method="post">
    <input type="checkbox" name="test" value="1">
    <input type="hidden" name="a" value="1">
    <input type="submit" value="Update">
</form>

instructions error

in examples/httpserver_handler_serves_file.py

line 13

change /static to /www

Thanks

Pico W EOPNOTSUPP On CircuitPython 9.0.0

Starting server on Pico W errors out on server.start(<str:ip>,<int:port>) when running on CircuitPython 9.0.0 with 9.x httpserver library. Error was: OSError: [Errno 95] EOPNOTSUPP. Same code runs on 8.2.10 without issue with 8.x library.

Steps to recreate:

  1. Install CircuitPython 9.0.0 on Pico W
  2. Load adafruit_httpserver 9.x library to /<path_to_drive>/CIRCUITPYTHON/lib
  3. Run code.py

Code adapted from Adafruit Pico W HTTP Server (https://learn.adafruit.com/pico-w-http-server-with-circuitpython/code-the-pico-w-http-server):

code.py

import os
import time
import ipaddress
import wifi
import socketpool
import busio
import board
import microcontroller
import terminalio
from digitalio import DigitalInOut, Direction
from adafruit_httpserver import Server, Request, Response, POST

#  onboard LED setup
led = DigitalInOut(board.LED)
led.direction = Direction.OUTPUT
led.value = False

#  connect to network
print()
print("Connecting to WiFi")
connect_text = "Connecting..."

#  set static IP address
ipv4 =  ipaddress.IPv4Address("192.168.1.25")
netmask =  ipaddress.IPv4Address("255.255.255.0")
gateway =  ipaddress.IPv4Address("192.168.1.1")
wifi.radio.set_ipv4_address(ipv4=ipv4,netmask=netmask,gateway=gateway)
#  connect to your SSID
wifi.radio.connect(os.getenv('CIRCUITPY_WIFI_SSID'), os.getenv('CIRCUITPY_WIFI_PASSWORD'))

print("Connected to WiFi")
pool = socketpool.SocketPool(wifi.radio)
server = Server(pool, "/static", debug=True)

#  variables for HTML

#  font for HTML
font_family = "monospace"

#  the HTML script
#  setup as an f string
#  this way, can insert string variables from code.py directly
#  of note, use {{ and }} if something from html *actually* needs to be in brackets
#  i.e. CSS style formatting
def webpage():
    html = f"""
    <!DOCTYPE html>
    <html>
    <head>
    <meta http-equiv="Content-type" content="text/html;charset=utf-8">
    <meta name="viewport" content="width=device-width, initial-scale=1">
    <style>
    html{{font-family: {font_family}; background-color: lightgrey;
    display:inline-block; margin: 0px auto; text-align: center;}}
      h1{{color: deeppink; width: 200; word-wrap: break-word; padding: 2vh; font-size: 35px;}}
      p{{font-size: 1.5rem; width: 200; word-wrap: break-word;}}
      .button{{font-family: {font_family};display: inline-block;
      background-color: black; border: none;
      border-radius: 4px; color: white; padding: 16px 40px;
      text-decoration: none; font-size: 30px; margin: 2px; cursor: pointer;}}
      p.dotted {{margin: auto;
      width: 75%; font-size: 25px; text-align: center;}}
    </style>
    </head>
    <body>
    <title>Pico W HTTP Server</title>
    <h1>Pico W HTTP Server</h1>
    <br>
    <p class="dotted">This is a Pico W running an HTTP server with CircuitPython.</p>
    <br>
    <h1>Control the LED on the Pico W with these buttons:</h1><br>
    <form accept-charset="utf-8" method="POST">
    <button class="button" name="LED ON" value="ON" type="submit">LED ON</button></a></p></form>
    <p><form accept-charset="utf-8" method="POST">
    <button class="button" name="LED OFF" value="OFF" type="submit">LED OFF</button></a></p></form>
    </body></html>
    """
    return html

#  route default static IP
@server.route("/")
def base(request: Request):  # pylint: disable=unused-argument
    #  serve the HTML f string
    #  with content type text/html
    return Response(request, f"{webpage()}", content_type='text/html')

#  if a button is pressed on the site
@server.route("/", POST)
def buttonpress(request: Request):
    #  get the raw text
    raw_text = request.raw_request.decode("utf8")
    print(raw_text)
    #  if the led on button was pressed
    if "ON" in raw_text:
        #  turn on the onboard LED
        led.value = True
    #  if the led off button was pressed
    if "OFF" in raw_text:
        #  turn the onboard LED off
        led.value = False
    #  reload site
    return Response(request, f"{webpage()}", content_type='text/html')

print("starting server..")
# startup the server
try:
    server.start(str(wifi.radio.ipv4_address), 80)
    print("Listening on http://%s:80" % wifi.radio.ipv4_address)
#  if the server fails to begin, restart the pico w
except Exception as e:
    print(f'Encountered exception.\n{e}\n')
    time.sleep(5)
    # print("restarting..")
    # microcontroller.reset()
ping_address = ipaddress.ip_address("8.8.4.4")


clock = time.monotonic() #  time.monotonic() holder for server ping

while True:
    try:
        #  every 30 seconds, ping server & update temp reading
        if (clock + 30) < time.monotonic():
            if wifi.radio.ping(ping_address) is None:
                print("lost connection")
            else:
                print("connected")
            clock = time.monotonic()
        #  poll the server for incoming/outgoing requests
        server.poll()
    # pylint: disable=broad-except
    except Exception as e:
        print(e)
        continue

Connection refused after some time/multiple requests on Wiznet5k Pico W5100S

Adafruit CircuitPython 8.0.3 on 2023-02-23; Raspberry Pi Pico with rp2040
I am building HTTP server with W5100S and can't get this library to work properly because of adafruit_wiznet5k_socket, i think. I cut some code to demonstrate the problem, so if you need explanation in something, i'll explain
code.py

import board
import busio
import digitalio
import time

import adafruit_wiznet5k.adafruit_wiznet5k_socket as socket
from adafruit_wiznet5k.adafruit_wiznet5k import WIZNET5K
from adafruit_httpserver.request import HTTPRequest
from adafruit_httpserver.response import HTTPResponse
from adafruit_httpserver.server import HTTPServer
from adafruit_httpserver.methods import HTTPMethod

IP_ADDRESS = (192, 168, 1, 244)
SUBNET_MASK = (255, 255, 255, 0)
GATEWAY_ADDRESS = (192, 168, 1, 1)
DNS_SERVER = (8, 8, 8, 8)
W5x00_RSTn = board.GP20
ethernetRst = digitalio.DigitalInOut(W5x00_RSTn)
ethernetRst.direction = digitalio.Direction.OUTPUT
ethernetRst.value = False
time.sleep(1)
ethernetRst.value = True

SPI0_SCK = board.GP18
SPI0_TX = board.GP19
SPI0_RX = board.GP16
SPI0_CSn = board.GP17
spi_bus = busio.SPI(SPI0_SCK, MOSI=SPI0_TX, MISO=SPI0_RX)

cs = digitalio.DigitalInOut(SPI0_CSn)
eth = WIZNET5K(spi_bus, cs, is_dhcp=False)


def routes(server):
    @server.route("/", HTTPMethod.POST)
    def base(request: HTTPRequest):
        raw_text = request.body.decode("UTF-8")
        print(raw_text)
        with open('index.html', 'r') as f:
            html_string = f.read()
        f.close()
        with HTTPResponse(request) as response:
            response.send(html_string, content_type="text/html")

    @server.route("/", HTTPMethod.GET)
    def base(request: HTTPRequest):
        with open('index.html', 'r') as f:
            html_string = f.read()
        f.close()
        with HTTPResponse(request) as response:
            response.send(html_string, content_type="text/html")


def raise_serv():
    eth.ifconfig = (IP_ADDRESS, SUBNET_MASK, GATEWAY_ADDRESS, DNS_SERVER)

    socket.set_interface(eth)
    server_ip = eth.pretty_ip(eth.ip_address)

    print(f"Listening on http://{server_ip}:80")
    return HTTPServer(socket), server_ip


while True:
    print("Raising server..")
    server, server_ip = raise_serv()
    routes(server)
    server.start(str(server_ip))
    print("Server raised.")
    while True:
        try:
            server.poll()
        except OSError as error:
            print(error)
            continue

index.html

<!DOCTYPE html>
<html lang="en">
<head>
  <meta charset="UTF-8">
  <meta http-equiv="X-UA-Compatible" content="IE=edge">
  <meta name="viewport" content="width=device-width, initial-scale=1.0">
  <title>Document</title>
  <style type="text/css">
    body {
      margin: 0;
    }
    .wrapper {
      background: #ffffff;
      box-sizing: border-box;
      margin: 0 auto;
      max-width: 1300px;
      min-height: 100vh;
    }
    .container {
      display: grid;
      grid-template-columns: 340px repeat(6, auto);
    }
    .el {
      padding: 2px;
      font-size: 17px;
    }
    input {
      text-align: right;
    }
    .btn-apply {
      text-align: right;
      padding: 15px 2px;
    }
    .row-border{
    border-top: 1px solid rgb(191, 252, 198);
    grid-column: 1 / 8;
    }
    .square {
      position: relative;
      padding-left: 17px;
      margin-left: 5px;
    }
    .square:after {
      position:absolute;
      content:"";
      width:17px;
      height:17px;
      top:2px;
      left:0;
    }
    .square-green {
      background-color:rgb(0, 190, 0);
    }
    .square-red {
      background-color:rgb(190, 0, 0);
    }
  </style>
</head>
<body>
  <div class="wrapper">
  <form action="" method="post">
    <div class="container">
      <div class="el el-1"><b></b></div>
      <div class="el el-2" align="right"><b>1</b></div>
      <div class="el el-3" align="right"><b>2</b></div>
      <div class="el el-4" align="right"><b>3</b></div>
      <div class="el el-5" align="right"><b>4</b></div>
      <div class="el el-6" align="right"><b>5</b></div>
      <div class="el el-7" align="right"><b>6</b></div>
      
      <div class="row-border"></div><div class="el el-1"></div>
      <div class="el el-2"><input name="name1" type="text" value="1" style="width: 95%; color:rgb(0, 150, 17)"></div>
      <div class="el el-3"><input name="name2" type="text" value="2" style="width: 95%; color:rgb(0, 150, 17)"></div>
      <div class="el el-4"><input name="name3" type="text" value="3" style="width: 95%; color:rgb(0, 150, 17)"></div>
      <div class="el el-5"><input name="name4" type="text" value="4" style="width: 95%; color:rgb(0, 150, 17)"></div>
      <div class="el el-6"><input name="name5" type="text" value="5" style="width: 95%; color:rgb(0, 150, 17)"></div>
      <div class="el el-7"><input name="name6" type="text" value="6" style="width: 95%; color:rgb(0, 150, 17)"></div>
      <div class="row-border"></div>

      <div class="el el-1"></div>
      <div class="el el-2" align="right">1<span class="square square-$COLOR1"></span></div>
      <div class="el el-3" align="right">1<span class="square square-$COLOR2"></span></div>
      <div class="el el-4" align="right">1<span class="square square-$COLOR3"></span></div>
      <div class="el el-5" align="right">1<span class="square square-$COLOR4"></span></div>
      <div class="el el-6" align="right">1<span class="square square-$COLOR5"></span></div>
      <div class="el el-7" align="right">1<span class="square square-$COLOR6"></span></div>
      <div class="row-border"></div>

      <div class="el el-1"></div>
      <div class="el el-2"></div>
      <div class="el el-3"></div>
      <div class="el el-4"></div>
      <div class="el el-5"></div>
      <div class="el el-6"></div>
      <div class="el el-7"></div>
      <div class="row-border"></div>

      <div class="el el-1" style="padding-left: 10px;"></div>
      <div class="el el-2" align="right"><input type="radio" name="log_level1" value="1" id="" $CHECKEDTRUE1></div>
      <div class="el el-3" align="right"><input type="radio" name="log_level2" value="1" id="" $CHECKEDTRUE2></div>
      <div class="el el-4" align="right"><input type="radio" name="log_level3" value="1" id="" $CHECKEDTRUE3></div>
      <div class="el el-5" align="right"><input type="radio" name="log_level4" value="1" id="" $CHECKEDTRUE4></div>
      <div class="el el-6" align="right"><input type="radio" name="log_level5" value="1" id="" $CHECKEDTRUE5></div>
      <div class="el el-7" align="right"><input type="radio" name="log_level6" value="1" id="" $CHECKEDTRUE6></div>
      <div class="row-border"></div>

      <div class="el el-1" style="padding-left: 10px;"></div>
      <div class="el el-2" align="right"><input type="radio" name="log_level1" value="0" id="" $CHECKEDFALSE1></div>
      <div class="el el-3" align="right"><input type="radio" name="log_level2" value="0" id="" $CHECKEDFALSE2></div>
      <div class="el el-4" align="right"><input type="radio" name="log_level3" value="0" id="" $CHECKEDFALSE3></div>
      <div class="el el-5" align="right"><input type="radio" name="log_level4" value="0" id="" $CHECKEDFALSE4></div>
      <div class="el el-6" align="right"><input type="radio" name="log_level5" value="0" id="" $CHECKEDFALSE5></div>
      <div class="el el-7" align="right"><input type="radio" name="log_level6" value="0" id="" $CHECKEDFALSE6></div>
      <div class="row-border"></div>

      <div class="el el-1"></div>
      <div class="el el-2"></div>
      <div class="el el-3"></div>
      <div class="el el-4"></div>
      <div class="el el-5"></div>
      <div class="el el-6"></div>
      <div class="el el-7"></div>
      <div class="row-border"></div>
      <div class="el el-1" style="padding-left: 10px;"></div>
      <div class="el el-2"><input name="impulse_time1" type="text" style="width: 95%;" value="1"></div>
      <div class="el el-3"><input name="impulse_time2" type="text" style="width: 95%;" value="1"></div>
      <div class="el el-4"><input name="impulse_time3" type="text" style="width: 95%;" value="1"></div>
      <div class="el el-5"><input name="impulse_time4" type="text" style="width: 95%;" value="1"></div>
      <div class="el el-6"><input name="impulse_time5" type="text" style="width: 95%;" value="1"></div>
      <div class="el el-7"><input name="impulse_time6" type="text" style="width: 95%;" value="1"></div>
      <div class="row-border"></div>

      <div class="el el-1" style="padding-left: 10px;"></div>
      <div class="el el-2"><button name="impulse_line" value="1" style="width: 100%;">1</button></div>
      <div class="el el-3"><button name="impulse_line" value="2" style="width: 100%;">2</button></div>
      <div class="el el-4"><button name="impulse_line" value="3" style="width: 100%;">3</button></div>
      <div class="el el-5"><button name="impulse_line" value="4" style="width: 100%;">4</button></div>
      <div class="el el-6"><button name="impulse_line" value="5" style="width: 100%;">5</button></div>
      <div class="el el-7"><button name="impulse_line" value="6" style="width: 100%;">6</button></div>
      <div class="row-border"></div>
    </div>
    <div class="btn-apply">
      <button type="submit" name="apply_changes" value="1">a</button>
    </div>
    </form>
  </div>
</body>
</html>

After first request, server stops responding after some time, giving "ERR_CONNECTION_REFUSED" in browsers. Also, multiple POST requests(2 fast clicks on button) are doing the same, but faster. I've done some tests with different systems, that gave me no results. The only thing, that helped me is "Incognito" mode. Other versions of CircuitPython don't help.
When I interrupt programm with CTRL+C it gives.

  File "/lib/adafruit_httpserver/response.py", line 167, in send
  File "/lib/adafruit_httpserver/response.py", line 242, in _send_bytes
  File "/lib/adafruit_wiznet5k/adafruit_wiznet5k_socket.py", line 422, in send
  File "/lib/adafruit_wiznet5k/adafruit_wiznet5k.py", line 1034, in socket_write
  File "/lib/adafruit_wiznet5k/adafruit_wiznet5k.py", line 614, in write

Simple Test (examples/httpserver_simpletest_auto.py) error on M5stack Timer Camera X

Adafruit CircuitPython 8.2.9 on M5stack Timer Camera X.
I am building simple HTTP server.
I ran examples/httpserver_simpletest_auto.py( https://docs.circuitpython.org/projects/httpserver/en/latest/examples.html#id4 ) and got the following error.
AttributeError : 'SocketPool' object has no attribure 'SOL_SOCET' .
screen shot

I saw adafruitt_server/server.py line216 .
self.host, self.port = host, port

host is "192.168.10.116". port is 80.

Let me know if there is anything else to do.

How do I safely disable all access to filesystem?

This is likely a feature request, unless I've missed something.

In many cases I'd prefer my HTTPServer application define all possible valid URLs as server routes to python handlers, and any invalid URL request immediately returns a 404 error. No going to the file system when a route handler is not found.

In older versions of the library, I would create the server like this:
server = HTTPServer(pool, "undef")server = HTTPServer(pool)

And then use @server.route("/...") pragmas to define all URLs.

At some point since I last updated, the interface to HTTPServer changed so that it requires a second argument, that for root_path. In the documentation, the example is:
server = HTTPServer(pool, "/static")

I'd prefer to prevent any attempt to access the filesystem. I know I could pass a nonexistent file/folder as root_path, which would cause all filesystem I/O attempts to raise an error. But that would cause unnecessary file system I/O for every route handler miss, and perhaps more importantly, it is confusing to someone looking at the code. They'd wonder why the given root_path folder didn't exist. It also causes an error message, saying which file was not found, back to the client, which might encourage someone to try manipulating URL paths in an attempt to access "private" data on the filesystem.

My first thought was to try this:
server = HTTPServer(pool, None)

However, if a client requests a URL with path that has no route handler, HTTPServer.poll() will do an HTTPResponse.send_file(), which eventually does root_path.endswith(). This causes an AttributeError to be raised because None is not a string in this case. Would it be possible to have an option to instruct HTTPServer to never check the file system?

To do this, I'd suggest that HTTPServer.poll() checks if root_path is None before calling HTTPResponse.send_file() . If so, it would raise FileNotExistsError to directly cause a 404 response, without going to the filesystem.

Thank you for your consideration, and for all of your hard work making this software.

Edited for typos. And clarity.

Allow Response to send bytes

A use case came up on Discord (sending jpeg from memory, not a file) for sending bytes rather than a string.

Currently send and send_chunk expect strings, but perhaps with either a parameter or type checking, either could be handled.

Workaround is to use _send_bytes().

Addendum: bytearray, bytes, or memoryview; anything sockets support

Allow setting of HTTP headers in HTTP response

Currently it's not possible (it seems) to set custom HTTP headers in the response.
This means using content/data from a adafruit_httpserver device (say using XmlHttpRequest) is not possible because the CORS header Access-Control-Allow-Origin: * cannot be set.

HTTPS Server now works on ESP32-S3 but MemoryError on Pico W and ESP32-S2

Filing here first, though it could be a core issue and get moved.

An HTTPS server was published by a community member in early 2023 that worked on Pico W (but ran a bit slow):
https://github.com/ide/circuitpython-https-server

Then there was recently some core development to make HTTPS Server work on espressif boards:
adafruit/circuitpython#8268
adafruit/circuitpython#8932
adafruit/circuitpython#8962
(plus further changes to require explicit SO_REUSEADDR)

However, HTTPS Server seems to have broken on Pico W with MemoryError, somewhere between 8.0.0 and 8.1.0 (bisect of CP versions, but using latest 4.5.5 adafruit_HTTPServer).

Also, although HTTPS Server does now run well on ESP32-S3 boards, ESP32-S2 boards (with PSRAM) also get the MemoryError.

The MemoryError occurs in poll, when calling socket.accept():

Traceback (most recent call last):
  File "code.py", line 72, in <module>
  File "adafruit_httpserver/server.py", line 450, in poll
  File "adafruit_httpserver/server.py", line 404, in poll
MemoryError: 

Boards tested:

Adafruit CircuitPython 9.0.0-beta.2-4-gf23205c822 on 2024-02-20; Adafruit QT Py ESP32-S3 4MB Flash 2MB PSRAM with ESP32S3
Adafruit CircuitPython 9.0.0-beta.2-4-gf23205c822 on 2024-02-20; Adafruit QT Py ESP32S2 with ESP32S2
Adafruit CircuitPython 9.0.0-beta.2-4-gf23205c822 on 2024-02-20; Raspberry Pi Pico W with rp2040

"ImportError: no module named 'hashlib'" on the WIZnet W5500-EVB-Pico

Hi.

When I import the adafruit_httpserver on the WIZnet W5500-EVB-Pico, I got following error.

Adafruit CircuitPython 8.2.9 on 2023-12-06; W5500-EVB-Pico with rp2040
>>> from adafruit_httpserver import Server, Request, Response, MIMEType
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
  File "adafruit_httpserver/__init__.py", line 56, in <module>
  File "adafruit_httpserver/response.py", line 20, in <module>
ImportError: no module named 'hashlib'

The error does not occur in the version I downloaded around May.
I seem to import hashlib was added at the commit of 1e1ad58 .

How can I solve this problem?

CircuitPython 9.0.0 will require SO.REUSEADDR for socket reuse

CircuitPython 9.0.0-beta.1 will include this PR:

This makes the semantics of socket reuse as it is in CPython. One would need to use socket.setsockopt(pool.SOL_SOCKET, pool.SO_REUSEADDR, 1) in the same cases one would use it in CPython.

HTTPServer maintainers: Is this a breaking change for use of adafruit_httpserver or for the examples? Thanks for looking.

Content-Length and multibyte characters in HTTPResponse

I am building a tiny web server using adafruit_httpserver.server and circuitpython 8.0.0-beta.5 on ESP32-S2.
When multibyte characters (Japanese) are included in response body, the content-length header value received by a web client looks shorter, and some tailing characters in the body are missing.

page_html = """
<html>
  <head><title>test</title></head>
  <body>
   some text in multibyte language here..
  </body>
</html>
"""

@server.route("/test")
def base(request):
  return HTTPResponse(content_type='text/html;charset=UTF-8',body=page_html)
$ curl -v http://192.168.xx.xx/test
 :
< HTTP/1.1 200 OK
< Content-Length: 117
< Content-Type: text/html;charset=UTF-8
< Connection: close
<
* Excess found in a read: excess = 10, size = 117, maxdownload = 117, bytecount = 0
(html response are shown, but the last several characters in body are missing)

Looking into adafruit_httpserver/response.py, content-length is calculated as len(body), as in,
response_headers.setdefault("Content-Length", content_length or len(body))
and the whole response are sent after converted into bytes.
If I replace it with len(body.encode("utf-8")), the above trouble disappears, but I'm not sure this modification is right.

response.py#L78

Yielding data from an async task to a chunked response

How do I yield data to a chunked response from a async routine?

The code reads the adc like this:

async def read_adc(pin: microcontroller.Pin):
    with analogio.AnalogIn(pin) as adc:
        while True:
            local_value = (adc.value * 3.3) / 65536
            await asyncio.sleep(0.1) 

How can the local_value be yield'ed in a chunked response?

Pico W stuck in an infinite loop

If I do the GET request too quickly on a PICO W it gets stuck in an infinite loop.

I took inspiration from this issue on how to debug: #41

When I add print(bytes_sent, bytes_to_send) to the response.py _send_bytes method I can see it gets stuck in an infinite loop.

Hard to capture in text as the board locks up and so does Mu serial connection I took a video and I've attached an image from the point where the issue happens.

code.py at the moment is:

import socketpool
import wifi
import os
from adafruit_httpserver.mime_type import MIMEType
from adafruit_httpserver.request import HTTPRequest
from adafruit_httpserver.response import HTTPResponse
from adafruit_httpserver.server import HTTPServer


if not wifi.radio.ipv4_address:
    ssid = os.getenv('WIFI_SSID')
    print(f"Connecting to Wifi: {ssid}")
    wifi.radio.connect(ssid, os.getenv('WIFI_PASSWORD'))
    print(f"Connected: IP address is {wifi.radio.ipv4_address}")



pool = socketpool.SocketPool(wifi.radio)
server = HTTPServer(pool)
server.socket_timeout = 0.25

count = 0
poll_count = 0

@server.route("/")
def base(request: HTTPRequest):
    print(request.path, request.connection)
    """
    Serve the default index.html file.
    """
    with HTTPResponse(request, content_type=MIMEType.TYPE_HTML) as response:
        response.send_file("index.html")


@server.route("/MUTE")
def mute(request: HTTPRequest):
    print(request.path, request.connection)
    """
    Serve the default index.html file.
    """
    try:
        global count
        count = count + 1
        print(f"{count} sending")
        # device.send(0xE2)
        with HTTPResponse(request, content_type=MIMEType.TYPE_HTML) as response:
            response.send_file("index.html")
        print(f"{count} sent")
    except Exception as e:
        print(e)


print(f"Listening on http://{wifi.radio.ipv4_address}:80")
# Start the server.
server.start(str(wifi.radio.ipv4_address))
while True:
    try:
        poll_count = poll_count + 1
        if(poll_count % 10_000 == 0): 
            print(f"Polling {poll_count}")
        server.poll()
        if(poll_count % 10_000 == 0): 
            print(f"Poll Done {poll_count}")
    except Exception as error:
        print(error)
        raise error

and index.html is:

<!DOCTYPE html>
<html lang="en">
<head>
  <title>REMOTE</title>
  <meta charset="utf-8">
  <meta name="viewport" content="width=device-width, initial-scale=1">
  <link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/[email protected]/dist/css/bootstrap.min.css" integrity="sha384-Gn5384xqQ1aoWXA+058RXPxPg6fy4IWvTNh0E263XmFcJlSAwiGgFAW/dAiS6JXm" crossorigin="anonymous">
</head>
<body class="bg-dark">
    <div class="container-sm bg-dark rounded text-center">
            <br>
            <div class="row">
                <div class="col-1">
                    <div class="text-light">
                      <a class="btn brn-dark btn-lg text-light btn-outline-primary" href="/MUTE" role="button">&nbsp;&#128264;&nbsp;</a>
                    </div>
                </div>
            </div>
    </div>
</body>
</html>

image

CircuitPython clients can't complete requests to HTTPServer

Trying to use adafruit_requests to connect either the 5100S-based WIZnet Pico EVB running Adafruit CircuitPython 7.2.5 on 2022-04-06; Raspberry Pi Pico with rp2040 or the Adafruit Feather RP2040 with Adafruit Ethernet FeatherWing running Adafruit CircuitPython 7.2.5 on 2022-04-06; Adafruit Feather RP2040 with rp2040 to an Adafruit Feather ESP32-S2 TFT running HTTPServer on an IPv4 (this repo simpletest example; no mDNS/hostname/FQDN involved) results in one of the following exception traces in all cases:

Either (less often):

Traceback (most recent call last):
  File "code.py", line 211, in <module>
  File "adafruit_requests.py", line 815, in get
  File "adafruit_requests.py", line 685, in request
OutOfRetries: Repeated socket failures

Or (more often):

Traceback (most recent call last):
  File "code.py", line 211, in <module>
  File "adafruit_requests.py", line 815, in get
  File "adafruit_requests.py", line 661, in request
  File "adafruit_requests.py", line 529, in _get_socket
  File "adafruit_wiznet5k/adafruit_wiznet5k_socket.py", line 251, in connect
  File "adafruit_wiznet5k/adafruit_wiznet5k.py", line 574, in socket_connect
RuntimeError: Failed to establish connection.

I was initially going to file this issue in WIZnet, but a sanity check of trying to connect to the HTTPServer from ESP32-S2 (e.g., Adafruit Feather ESP32-S2 TFT) also gets an exception every time, after about a minute. That surprised me, I may be doing something wrong. Maybe it's a Requests issue.

ESP32-S2 Client Code:

import traceback
import wifi
import socketpool
import ssl
import adafruit_requests
from adafruit_httpserver import HTTPServer, HTTPResponse
from secrets import secrets

wifi.radio.connect(secrets['ssid'], secrets['password'])
pool = socketpool.SocketPool(wifi.radio)
requests = adafruit_requests.Session(pool, ssl.create_default_context())

URLS = [
    "http://wifitest.adafruit.com/testwifi/index.html",
    "http://192.168.5.32",   # LAN Apache server
    "http://192.168.6.164",  # LAN ESP32-S2 with adafruit_httpserver
]

for url in URLS:
    try:
        print(url)
        with requests.get(url) as response:
            print(response.status_code, response.reason)
    except Exception as ex:
        traceback.print_exception(ex, ex, ex.__traceback__)

Output:

code.py output:
http://wifitest.adafruit.com/testwifi/index.html
200 bytearray(b'OK')
http://192.168.5.32
200 bytearray(b'OK')
http://192.168.6.164
Traceback (most recent call last):
  File "code.py", line 22, in <module>
  File "adafruit_requests.py", line 720, in get
  File "adafruit_requests.py", line 661, in request
  File "adafruit_requests.py", line 512, in _get_socket
RuntimeError: Sending request failed

Both Espressif client and Espressif server are running:
Adafruit CircuitPython 7.2.5 on 2022-04-06; Adafruit Feather ESP32-S2 TFT with ESP32S2

Connecting to the HTTPServer from a browser or curl works fine.

Connecting to local Apache server at an IPv4 from any of these clients works fine.

PyPi Release not importable

It seems that the files being released on PyPi don't include the actual library, or are not importable for some reason.

โฏ python -m pip install adafruit-circuitpython-httpserver
Collecting adafruit-circuitpython-httpserver
  Using cached adafruit_circuitpython_httpserver-4.0.1-py3-none-any.whl (3.8 kB)
Collecting Adafruit-Blinka (from adafruit-circuitpython-httpserver)
  Downloading Adafruit_Blinka-8.19.0-py3-none-any.whl (302 kB)
     โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ” 302.3/302.3 kB 6.2 MB/s eta 0:00:00
Collecting Adafruit-PlatformDetect>=3.13.0 (from Adafruit-Blinka->adafruit-circuitpython-httpserver)
  Downloading Adafruit_PlatformDetect-3.46.0-py3-none-any.whl (23 kB)
Collecting Adafruit-PureIO>=1.1.7 (from Adafruit-Blinka->adafruit-circuitpython-httpserver)
  Downloading Adafruit_PureIO-1.1.11-py3-none-any.whl (10 kB)
Collecting pyftdi>=0.40.0 (from Adafruit-Blinka->adafruit-circuitpython-httpserver)
  Using cached pyftdi-0.54.0-py3-none-any.whl (144 kB)
Collecting adafruit-circuitpython-typing (from Adafruit-Blinka->adafruit-circuitpython-httpserver)
  Downloading adafruit_circuitpython_typing-1.9.4-py3-none-any.whl (10 kB)
Collecting pyusb!=1.2.0,>=1.0.0 (from pyftdi>=0.40.0->Adafruit-Blinka->adafruit-circuitpython-httpserver)
  Using cached pyusb-1.2.1-py3-none-any.whl (58 kB)
Collecting pyserial>=3.0 (from pyftdi>=0.40.0->Adafruit-Blinka->adafruit-circuitpython-httpserver)
  Using cached pyserial-3.5-py2.py3-none-any.whl (90 kB)
Collecting adafruit-circuitpython-busdevice (from adafruit-circuitpython-typing->Adafruit-Blinka->adafruit-circuitpython-httpserver)
  Downloading adafruit_circuitpython_busdevice-5.2.6-py3-none-any.whl (7.5 kB)
Collecting adafruit-circuitpython-requests (from adafruit-circuitpython-typing->Adafruit-Blinka->adafruit-circuitpython-httpserver)
  Downloading adafruit_circuitpython_requests-1.13.4-py3-none-any.whl (11 kB)
Collecting typing-extensions~=4.0 (from adafruit-circuitpython-typing->Adafruit-Blinka->adafruit-circuitpython-httpserver)
  Using cached typing_extensions-4.6.2-py3-none-any.whl (31 kB)
Installing collected packages: pyserial, Adafruit-PlatformDetect, typing-extensions, pyusb, Adafruit-PureIO, pyftdi, adafruit-circuitpython-requests, adafruit-circuitpython-busdevice, adafruit-circuitpython-typing, Adafruit-Blinka, adafruit-circuitpython-httpserver
Successfully installed Adafruit-Blinka-8.19.0 Adafruit-PlatformDetect-3.46.0 Adafruit-PureIO-1.1.11 adafruit-circuitpython-busdevice-5.2.6 adafruit-circuitpython-httpserver-4.0.1 adafruit-circuitpython-requests-1.13.4 adafruit-circuitpython-typing-1.9.4 pyftdi-0.54.0 pyserial-3.5 pyusb-1.2.1 typing-extensions-4.6.2

CircuitPython_RGB_LED_HTTPServer on ๎‚  main [!+] is ๐Ÿ“ฆ v0.0.0+auto.0 via ๐Ÿ v3.10.6 (venv) took 2s 
โฏ python
Python 3.10.6 (main, Mar 10 2023, 10:55:28) [GCC 11.3.0] on linux
Type "help", "copyright", "credits" or "license" for more information.
>>> from adafruit_httpserver import Server
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
ModuleNotFoundError: No module named 'adafruit_httpserver'

It installs successfully, but if you try to import it there is a ModuleNotFoundError raised. My environment was Ubuntu PC. I ran into this issue while trying to build the docs locally for a library that depends on this one. Perhaps it's something specific to my environment, since the docs seem to have built successfully via actions here in github.

Adding cors headers messes up content-type and content-length

Given the following object:

headers = {
    "Access-Control-Allow-Headers": "*",
    "Access-Control-Allow-Origin": "*",
    "Access-Control-Allow-Methods": "GET,POST,DELETE"
}

And the following server function:

@server.route("/api/v1/info", HTTPMethod.GET)
def info(request):
    """Return info"""
    try:
        obj = {"version": Api.api_version, "name": Api.api_name}
        response = json.dumps(obj)
        return HTTPResponse(status=CommonHTTPStatus.OK_200, body=response, content_type=MIMEType.TYPE_JSON, headers=Api.headers)
    except:
        return HTTPResponse(status=CommonHTTPStatus.BAD_REQUEST_400, headers=Api.headers)

The content-type gets turned to octet-stream and the content-length gets set to 1024.

doing this in the response.py code in the _construct_response_bytes function works fine:

headers.setdefault("Access-Control-Allow-Headers", "*")
headers.setdefault("Access-Control-Allow-Origin", "*")
headers.setdefault("Access-Control-Allow-Methods", "*")

this is a temporary solution, but not very preferred. I have looked around in the code and I have no idea what causes this issue

Documentation: `status` in response cannot be a tuple

Hello,

I started using this library today and followed the documentation describing how to set the status code when responding to a request. The triple-quoted docstring for HTTPResponse.__init__ describes status as:

    :param tuple status: The HTTP status code to return, as a tuple of (int, "message").
     Common statuses are available in `HTTPStatus`.

while the type hint for the constructor argument defines it as status: tuple = HTTPStatus.OK. The name tuple is recognized by the documentation generator, which creates a link to the Python.org documentation for the tuple type.

Passing a tuple does not actually work here, and causes the response to be badly formatted. The value given to status is saved in self.status, then passed to self._send_response, which in turn calls self._send_bytes with the value produced by self._HEADERS_FORMAT.format:

def _send_response(self, conn, status, content_type, body):
self._send_bytes(
conn, self._HEADERS_FORMAT.format(status, content_type, len(body))
)
self._send_bytes(conn, body)

This format method is str.format, since self._HEADERS_FORMAT is just a string:

_HEADERS_FORMAT = (
"HTTP/1.1 {}\r\n"
"Content-Type: {}\r\n"
"Content-Length: {}\r\n"
"Connection: close\r\n"
"\r\n"
)

Passing a tuple as documented โ€“ e.g. (200, 'OK') causes it to be rendered at the position of the first {} in the format string, making the response look like this:

HTTP/1.1 (200, 'OK')
Content-Type: text/plain
Content-Length: 5
Connection: close

hello

This is not a valid HTTP response, so when curl receives it it fails with

* Unsupported HTTP version in response
* Closing connection 0

The docs do suggest to use the common values pre-defined as static fields in HTTPStatus, which aren't tuples but HTTPStatus instances. They are only provided for status code 200, 404, and 500, so it's likely that users would want to provide other values and try to use tuples like (403, 'Forbidden') for example.
HTTPStatus instances are rendered correctly in the format string because the class specifically defines its string representation with a __str__ method:

def __str__(self):
return f"{self.value} {self.phrase}"

I'm not sure whether this should be best handled as purely a documentation change โ€“ no longer suggesting tuple โ€“ or as a code change, actually supporting tuple and rendering it correctly, maybe even raising an exception if a tuple is provided that is not made of an int and a string. I'm far from an authority on the matter, but it seems more Pythonic to me to actually support tuples rather than require an HTTPStatus object or a protocol-formatted string to directly inject into the response.

Thanks for this library!

Pico W need too much time for receiving request data in random cases

I have a for example a small web page with repeated ajax requests. Page is sending one request per second and server on Pico W needs different times for processing same request. In my case it varies between 40 ms and 250 ms.

I expected the time for processing the request to be approximately in the range of 40 to 80ms.

Connection to Pico is fast and when i added some debug prints to poll() function in server.py, then i found, that the bottleneck is function - self._receive_header_bytes(conn)

   def poll(self):
        try:
            conn, client_address = self._sock.accept()

            with conn:
                conn.settimeout(self._timeout)

                start_msecs = supervisor.ticks_ms()

                # Receiving data until empty line
                header_bytes = self._receive_header_bytes(conn)

                print(f"time to receive header bytes: {supervisor.ticks_ms() - start_msecs} ms")

                # Return if no data received

example of printed output (it's not the same sample as shown in image)

time to receive header bytes: 157 ms
time to receive header bytes: 1 ms
time to receive header bytes: 152 ms
time to receive header bytes: 1 ms
time to receive header bytes: 129 ms
time to receive header bytes: 1 ms
time to receive header bytes: 12 ms
time to receive header bytes: 95 ms
time to receive header bytes: 210 ms
time to receive header bytes: 1 ms
time to receive header bytes: 12 ms
time to receive header bytes: 1 ms
time to receive header bytes: 46 ms
time to receive header bytes: 1 ms
time to receive header bytes: 167 ms
time to receive header bytes: 188 ms
time to receive header bytes: 199 ms
time to receive header bytes: 208 ms

image1
image2

example code:

import os
import time
import ipaddress
import wifi
import socketpool

import microcontroller

from adafruit_httpserver.server import HTTPServer
from adafruit_httpserver.request import HTTPRequest
from adafruit_httpserver.response import HTTPResponse
from adafruit_httpserver.methods import HTTPMethod
from adafruit_httpserver.mime_type import MIMEType
from adafruit_httpserver.status import CommonHTTPStatus

#  set static IP address
ipv4 = ipaddress.IPv4Address("192.168.88.99")
netmask = ipaddress.IPv4Address("255.255.255.0")
gateway = ipaddress.IPv4Address("192.168.88.1")

wifi.radio.set_ipv4_address(ipv4=ipv4, netmask=netmask, gateway=gateway)
#  connect to your SSID
wifi.radio.connect(
    os.getenv("CIRCUITPY_WIFI_SSID"), os.getenv("CIRCUITPY_WIFI_PASSWORD")
)

print("Connected to WiFi")
pool = socketpool.SocketPool(wifi.radio)
server = HTTPServer(pool)

@server.route("/")
def base(request: HTTPRequest):  # pylint: disable=unused-argument
    with HTTPResponse(request, content_type=MIMEType.TYPE_HTML) as response:
        response.send("""
<html>
    <header>
        <script>
            const ajax = function (opt, cb) {
                const headers = opt.headers || {},
                    body = opt.body || "",
                    xhr = new XMLHttpRequest();

                xhr.open(opt.method || (body ? "POST" : "GET"), opt.url || "/", true);
                xhr.ontimeout = xhr.onabort = xhr.onerror = function (e) {
                    console.error("XHR error: ", e, "opt: ", opt);
                };
                xhr.onreadystatechange = function () {
                    if (xhr.readyState === 4) {
                        cb(xhr.status, xhr.response || xhr.responseText);
                    }
                };
                headers["Content-Type"] = headers["Content-Type"] || "application/x-www-form-urlencoded";
                for (const f in headers) {
                    xhr.setRequestHeader(f, headers[f]);
                }
                xhr.send(body);
            };

            function ajaxRequest(request, interval) {
                const ax = function () {
                    ajax(request, function (code, response) {
                        if (code == 200 && response) {
                            console.log("response", response);
                        }
                        if (interval) {
                            setTimeout(ax, interval);
                        }
                    });
                };
                ax();
            }

            data = {"id":"sw1","value":true}
            intervalMs = 1000

            ajaxRequest({ url: "/api", method: "POST", headers: { "Content-Type": "application/json" }, body: JSON.stringify(data) }, intervalMs);

        </script>
    </header>
    <body>
        sapmle page with ajax request every 1 second
    </body>
</html>""")


@server.route("/api", HTTPMethod.POST)
def base(request: HTTPRequest):
    print("api request", request.body)
    with HTTPResponse(request, content_type=MIMEType.TYPE_JSON) as response:
        response.send('{"status": "ok"}')


print("starting server..")
# startup the server
try:
    server.start(str(wifi.radio.ipv4_address))
    print("Listening on http://%s:80" % wifi.radio.ipv4_address)
    
#  if the server fails to begin, restart the pico w
except OSError:
    time.sleep(5)
    print("restarting..")
    microcontroller.reset()

while True:
    try:
        server.poll()

    except Exception as e:
        print(e)
        continue

HTTP Requests from Chrome block the server

There seems to be a problem handling requests from Chrome. Chrome locks the http server for a minute, blocking any other requests from being handled - other than requests from the same Chrome tab. A minute after the last request from Chrome, the server will start handling other requests again.

If you start a server, it will handle requests correctly from clients like node-red, firefox etc. (anything other than Chrome!).
If you are looping with server.poll() the loop will run correctly, but will freeze for 60 seconds after the Chrome request.
An example from an ESP32-S3 I am using:

pool = socketpool.SocketPool(wifi.radio)
server = HTTPServer(pool)
server.request_buffer_size = 2048
server.socket_timeout = 1
server.start(str(wifi.radio.ipv4_address))

@server.route("/")
def base(request: HTTPRequest):
    with HTTPResponse(request, content_type=MIMEType.TYPE_TXT, chunked=False) as response:
        try:
            print("REQUEST: /")
            response.send("HELLO")
        except OSError:
            response.send("ERROR")

while True:
    try:
        led.value = True
        time.sleep(0.05)
        led.value = False
        time.sleep(0.1)
        server.poll()
        
    except OSError as error:
        print(error)
        continue

As soon as you hit the server with Chrome, the server responds but stops the loop and stops handling requests from other clients. If you make another request from chrome in the same tab, it will be handled, and seemingly at that time pending requests from other clients can sometimes be handled but the loop will freeze again right after.

Making a request to an undefined path from Chrome freezes the loop also - you get the 404 in chrome and then server loop freezes - whatever code you put in your handler does not fix this problem.

A minute after the last Chrome request, the loop restarts and the server functions normally.

If you quit Chrome while the loop is locked, the loop restarts immediately.

I've tried a bunch of things to fix this, including closing the connection in the handler, sending various keep-alive responses to drop the connection - none work.

I think this needs an SDK level fix!

HTTPServer stops working on CircuitPython version >= 9.0

HTTPServer stops working on CircuitPython version >= 9.0

Around line 207 in server.py it is assumed that sock.setsockopt(socket_source.SOL_SOCKET, socket_source.SO_REUSEADDR, 1) would work as of CircuitPython version 9.0. Unfortunately it doesn't and raises an OSError: [Errno 95] EOPNOTSUPP.

Perhaps better in the circuitpython implementations to just try to set SO_REUSEADDR and catch + ignore the error.

Airlift Support?

The Metro M4 Lite running CircuitPython 9.0.0-beta2 does not contain the necessary socketpool library. Are the Airlift configurations going to be supported with HTTPServer?

forum thread for reference.

Discard or rewrite this library completely in favor of something with better structure and more functionality

This library was written in a hurry for a specific project that needed to serve large files. Most of the code was adapted from another simple HTTP server library.

It would be fine with me if this library were completely rewritten, or replaced with, say, https://github.com/deckerego/ampule, or some simple WSGI-ish server that also served files. There is nothing about the code here that is sacred. I've looked at flask and a couple of other "small" server libraries, but they don't seem small enough, or they have dependencies that are not small.

Recommend Projects

  • React photo React

    A declarative, efficient, and flexible JavaScript library for building user interfaces.

  • Vue.js photo Vue.js

    ๐Ÿ–– Vue.js is a progressive, incrementally-adoptable JavaScript framework for building UI on the web.

  • Typescript photo Typescript

    TypeScript is a superset of JavaScript that compiles to clean JavaScript output.

  • TensorFlow photo TensorFlow

    An Open Source Machine Learning Framework for Everyone

  • Django photo Django

    The Web framework for perfectionists with deadlines.

  • D3 photo D3

    Bring data to life with SVG, Canvas and HTML. ๐Ÿ“Š๐Ÿ“ˆ๐ŸŽ‰

Recommend Topics

  • javascript

    JavaScript (JS) is a lightweight interpreted programming language with first-class functions.

  • web

    Some thing interesting about web. New door for the world.

  • server

    A server is a program made to process requests and deliver data to clients.

  • Machine learning

    Machine learning is a way of modeling and interpreting data that allows a piece of software to respond intelligently.

  • Game

    Some thing interesting about game, make everyone happy.

Recommend Org

  • Facebook photo Facebook

    We are working to build community through open source technology. NB: members must have two-factor auth.

  • Microsoft photo Microsoft

    Open source projects and samples from Microsoft.

  • Google photo Google

    Google โค๏ธ Open Source for everyone.

  • D3 photo D3

    Data-Driven Documents codes.