uhttp-server 2.2.1__tar.gz → 2.3.0__tar.gz
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- uhttp_server-2.3.0/.github/workflows/micropython.yml +40 -0
- uhttp_server-2.3.0/.github/workflows/publish.yml +35 -0
- uhttp_server-2.3.0/.github/workflows/tests.yml +30 -0
- uhttp_server-2.3.0/.gitignore +10 -0
- {uhttp_server-2.2.1 → uhttp_server-2.3.0}/PKG-INFO +190 -8
- {uhttp_server-2.2.1 → uhttp_server-2.3.0}/README.md +189 -7
- uhttp_server-2.3.0/examples/client_with_server.py +123 -0
- uhttp_server-2.3.0/examples/http_to_https_redirect.py +85 -0
- uhttp_server-2.3.0/examples/https_server.py +57 -0
- {uhttp_server-2.2.1 → uhttp_server-2.3.0}/pyproject.toml +5 -2
- uhttp_server-2.3.0/tests/__init__.py +0 -0
- uhttp_server-2.3.0/tests/test_mpy_integration.py +330 -0
- uhttp_server-2.3.0/tests/test_mpy_websocket.py +488 -0
- uhttp_server-2.3.0/tests/test_websocket.py +817 -0
- uhttp_server-2.3.0/tests/test_websocket_utils.py +343 -0
- {uhttp_server-2.2.1 → uhttp_server-2.3.0}/uhttp/server.py +540 -6
- {uhttp_server-2.2.1 → uhttp_server-2.3.0}/uhttp_server.egg-info/PKG-INFO +190 -8
- {uhttp_server-2.2.1 → uhttp_server-2.3.0}/uhttp_server.egg-info/SOURCES.txt +12 -0
- {uhttp_server-2.2.1 → uhttp_server-2.3.0}/LICENSE +0 -0
- {uhttp_server-2.2.1 → uhttp_server-2.3.0}/setup.cfg +0 -0
- {uhttp_server-2.2.1 → uhttp_server-2.3.0}/tests/test_100_continue.py +0 -0
- {uhttp_server-2.2.1 → uhttp_server-2.3.0}/tests/test_concurrent_connections.py +0 -0
- {uhttp_server-2.2.1 → uhttp_server-2.3.0}/tests/test_content_length_security.py +0 -0
- {uhttp_server-2.2.1 → uhttp_server-2.3.0}/tests/test_data_parsing.py +0 -0
- {uhttp_server-2.2.1 → uhttp_server-2.3.0}/tests/test_double_response.py +0 -0
- {uhttp_server-2.2.1 → uhttp_server-2.3.0}/tests/test_eagain.py +0 -0
- {uhttp_server-2.2.1 → uhttp_server-2.3.0}/tests/test_error_handling.py +0 -0
- {uhttp_server-2.2.1 → uhttp_server-2.3.0}/tests/test_event_mode.py +0 -0
- {uhttp_server-2.2.1 → uhttp_server-2.3.0}/tests/test_ipv6.py +0 -0
- {uhttp_server-2.2.1 → uhttp_server-2.3.0}/tests/test_keepalive.py +0 -0
- {uhttp_server-2.2.1 → uhttp_server-2.3.0}/tests/test_keepalive_basic.py +0 -0
- {uhttp_server-2.2.1 → uhttp_server-2.3.0}/tests/test_keepalive_http10.py +0 -0
- {uhttp_server-2.2.1 → uhttp_server-2.3.0}/tests/test_keepalive_limits.py +0 -0
- {uhttp_server-2.2.1 → uhttp_server-2.3.0}/tests/test_keepalive_simple.py +0 -0
- {uhttp_server-2.2.1 → uhttp_server-2.3.0}/tests/test_multipart.py +0 -0
- {uhttp_server-2.2.1 → uhttp_server-2.3.0}/tests/test_pipelining.py +0 -0
- {uhttp_server-2.2.1 → uhttp_server-2.3.0}/tests/test_respond_file.py +0 -0
- {uhttp_server-2.2.1 → uhttp_server-2.3.0}/tests/test_respond_file_race.py +0 -0
- {uhttp_server-2.2.1 → uhttp_server-2.3.0}/tests/test_ssl.py +0 -0
- {uhttp_server-2.2.1 → uhttp_server-2.3.0}/tests/test_utils.py +0 -0
- {uhttp_server-2.2.1 → uhttp_server-2.3.0}/uhttp_server.egg-info/dependency_links.txt +0 -0
- {uhttp_server-2.2.1 → uhttp_server-2.3.0}/uhttp_server.egg-info/top_level.txt +0 -0
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
name: MicroPython Integration
|
|
2
|
+
|
|
3
|
+
on:
|
|
4
|
+
workflow_dispatch:
|
|
5
|
+
push:
|
|
6
|
+
branches: [main]
|
|
7
|
+
|
|
8
|
+
jobs:
|
|
9
|
+
unit-tests:
|
|
10
|
+
runs-on: ubuntu-latest
|
|
11
|
+
steps:
|
|
12
|
+
- uses: actions/checkout@v4
|
|
13
|
+
- uses: actions/setup-python@v5
|
|
14
|
+
with:
|
|
15
|
+
python-version: '3.13'
|
|
16
|
+
- run: pip install -e .
|
|
17
|
+
- run: python -m unittest discover tests/ -v
|
|
18
|
+
|
|
19
|
+
micropython:
|
|
20
|
+
runs-on: [self-hosted, Linux]
|
|
21
|
+
needs: unit-tests
|
|
22
|
+
strategy:
|
|
23
|
+
fail-fast: false
|
|
24
|
+
matrix:
|
|
25
|
+
device: [ESP32]
|
|
26
|
+
steps:
|
|
27
|
+
- uses: actions/checkout@v4
|
|
28
|
+
|
|
29
|
+
- name: Install packages
|
|
30
|
+
run: |
|
|
31
|
+
$HOME/actions-runner/.venv/bin/pip install -e .
|
|
32
|
+
$HOME/actions-runner/.venv/bin/pip install uhttp-client
|
|
33
|
+
|
|
34
|
+
- name: MicroPython tests (${{ matrix.device }})
|
|
35
|
+
run: |
|
|
36
|
+
export PATH="$HOME/actions-runner/.venv/bin:$PATH"
|
|
37
|
+
export MPY_TEST_PORT="$(cat $HOME/actions-runner/.config/mpytool/${{ matrix.device }})"
|
|
38
|
+
export MPY_WIFI_SSID="$(jq -r .ssid $HOME/actions-runner/.config/uhttp/wifi.json)"
|
|
39
|
+
export MPY_WIFI_PASSWORD="$(jq -r .password $HOME/actions-runner/.config/uhttp/wifi.json)"
|
|
40
|
+
python -m unittest tests.test_mpy_integration -v
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
name: Publish to PyPI
|
|
2
|
+
|
|
3
|
+
on:
|
|
4
|
+
release:
|
|
5
|
+
types: [published]
|
|
6
|
+
|
|
7
|
+
jobs:
|
|
8
|
+
publish:
|
|
9
|
+
runs-on: ubuntu-latest
|
|
10
|
+
environment: pypi
|
|
11
|
+
permissions:
|
|
12
|
+
id-token: write
|
|
13
|
+
|
|
14
|
+
steps:
|
|
15
|
+
- uses: actions/checkout@v4
|
|
16
|
+
|
|
17
|
+
- name: Set up Python
|
|
18
|
+
uses: actions/setup-python@v5
|
|
19
|
+
with:
|
|
20
|
+
python-version: "3.14"
|
|
21
|
+
|
|
22
|
+
- name: Install build dependencies
|
|
23
|
+
run: pip install build
|
|
24
|
+
|
|
25
|
+
- name: Install package
|
|
26
|
+
run: pip install .
|
|
27
|
+
|
|
28
|
+
- name: Run unit tests
|
|
29
|
+
run: python -m unittest discover -v tests/
|
|
30
|
+
|
|
31
|
+
- name: Build package
|
|
32
|
+
run: python -m build
|
|
33
|
+
|
|
34
|
+
- name: Publish to PyPI
|
|
35
|
+
uses: pypa/gh-action-pypi-publish@release/v1
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
name: Tests
|
|
2
|
+
|
|
3
|
+
on:
|
|
4
|
+
push:
|
|
5
|
+
branches: [main]
|
|
6
|
+
pull_request:
|
|
7
|
+
branches: [main]
|
|
8
|
+
|
|
9
|
+
jobs:
|
|
10
|
+
test:
|
|
11
|
+
runs-on: ${{ matrix.os }}
|
|
12
|
+
strategy:
|
|
13
|
+
max-parallel: 1
|
|
14
|
+
matrix:
|
|
15
|
+
os: [ubuntu-latest, windows-latest]
|
|
16
|
+
python-version: ["3.10", "3.14"]
|
|
17
|
+
|
|
18
|
+
steps:
|
|
19
|
+
- uses: actions/checkout@v4
|
|
20
|
+
|
|
21
|
+
- name: Set up Python ${{ matrix.python-version }}
|
|
22
|
+
uses: actions/setup-python@v5
|
|
23
|
+
with:
|
|
24
|
+
python-version: ${{ matrix.python-version }}
|
|
25
|
+
|
|
26
|
+
- name: Install package
|
|
27
|
+
run: pip install .
|
|
28
|
+
|
|
29
|
+
- name: Run unit tests
|
|
30
|
+
run: python -m unittest discover -v tests/
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: uhttp-server
|
|
3
|
-
Version: 2.
|
|
3
|
+
Version: 2.3.0
|
|
4
4
|
Summary: Micro HTTP server for Python and MicroPython
|
|
5
5
|
Author-email: Pavel Revak <pavelrevak@gmail.com>
|
|
6
6
|
License: MIT
|
|
@@ -29,6 +29,7 @@ Micro HTTP server for MicroPython and CPython.
|
|
|
29
29
|
- SSL/TLS for HTTPS connections
|
|
30
30
|
- IPv6 and dual-stack support
|
|
31
31
|
- Event mode for streaming large uploads
|
|
32
|
+
- WebSocket support (RFC 6455)
|
|
32
33
|
- Memory-efficient (~32KB RAM minimum)
|
|
33
34
|
|
|
34
35
|
## Installation
|
|
@@ -294,6 +295,10 @@ Parameters:
|
|
|
294
295
|
|
|
295
296
|
- Returns `True` if event mode is enabled
|
|
296
297
|
|
|
298
|
+
**`max_ws_message_length`** (kwarg)
|
|
299
|
+
|
|
300
|
+
- Maximum WebSocket message size before chunking (default: 64KB)
|
|
301
|
+
|
|
297
302
|
#### Methods:
|
|
298
303
|
|
|
299
304
|
**`event_write(self, sockets)`**
|
|
@@ -395,6 +400,18 @@ Parameters:
|
|
|
395
400
|
|
|
396
401
|
- Application storage attribute for request state (read-write)
|
|
397
402
|
|
|
403
|
+
**`is_websocket_request(self)`**
|
|
404
|
+
|
|
405
|
+
- True if request is a WebSocket upgrade request
|
|
406
|
+
|
|
407
|
+
**`is_websocket(self)`**
|
|
408
|
+
|
|
409
|
+
- True if connection is in WebSocket mode
|
|
410
|
+
|
|
411
|
+
**`ws_message(self)`**
|
|
412
|
+
|
|
413
|
+
- Last received WebSocket message (str for text frames, bytes for binary frames)
|
|
414
|
+
|
|
398
415
|
#### Methods:
|
|
399
416
|
|
|
400
417
|
**`headers_get(self, key, default=None)`**
|
|
@@ -442,6 +459,117 @@ Parameters:
|
|
|
442
459
|
- Read available data from buffer
|
|
443
460
|
- Returns: bytes or None if no data available
|
|
444
461
|
|
|
462
|
+
**`accept_websocket(self)`**
|
|
463
|
+
|
|
464
|
+
- Accept WebSocket upgrade. Event mode: switches to WS mode. Non-event mode: returns `WebSocket` object.
|
|
465
|
+
|
|
466
|
+
**`ws_send(self, data)`** (event mode, WebSocket)
|
|
467
|
+
|
|
468
|
+
- Send WebSocket message. `str` → text frame, `bytes` → binary frame.
|
|
469
|
+
|
|
470
|
+
**`ws_ping(self, data=b'')`** (event mode, WebSocket)
|
|
471
|
+
|
|
472
|
+
- Send WebSocket ping frame
|
|
473
|
+
|
|
474
|
+
**`ws_close(self, code=1000, reason='')`** (event mode, WebSocket)
|
|
475
|
+
|
|
476
|
+
- Send WebSocket close frame and close connection
|
|
477
|
+
|
|
478
|
+
|
|
479
|
+
## WebSocket Support
|
|
480
|
+
|
|
481
|
+
uHTTP supports WebSocket connections (RFC 6455) in both event mode and non-event mode.
|
|
482
|
+
|
|
483
|
+
### Event Mode (non-blocking, multiple connections)
|
|
484
|
+
|
|
485
|
+
```python
|
|
486
|
+
from uhttp.server import (
|
|
487
|
+
HttpServer, EVENT_REQUEST, EVENT_WS_REQUEST,
|
|
488
|
+
EVENT_WS_MESSAGE, EVENT_WS_CHUNK_FIRST,
|
|
489
|
+
EVENT_WS_CHUNK_NEXT, EVENT_WS_CHUNK_LAST,
|
|
490
|
+
EVENT_WS_PING, EVENT_WS_CLOSE
|
|
491
|
+
)
|
|
492
|
+
|
|
493
|
+
server = HttpServer(port=8080, event_mode=True)
|
|
494
|
+
|
|
495
|
+
while True:
|
|
496
|
+
client = server.wait()
|
|
497
|
+
if not client:
|
|
498
|
+
continue
|
|
499
|
+
|
|
500
|
+
if client.event == EVENT_WS_REQUEST:
|
|
501
|
+
client.accept_websocket()
|
|
502
|
+
|
|
503
|
+
elif client.event == EVENT_REQUEST:
|
|
504
|
+
client.respond({'status': 'ok'})
|
|
505
|
+
|
|
506
|
+
elif client.event == EVENT_WS_MESSAGE:
|
|
507
|
+
# str for text frames, bytes for binary frames
|
|
508
|
+
client.ws_send(client.ws_message) # echo
|
|
509
|
+
|
|
510
|
+
elif client.event == EVENT_WS_CHUNK_FIRST:
|
|
511
|
+
client.context = {'chunks': [client.ws_message]}
|
|
512
|
+
|
|
513
|
+
elif client.event == EVENT_WS_CHUNK_NEXT:
|
|
514
|
+
client.context['chunks'].append(client.ws_message)
|
|
515
|
+
|
|
516
|
+
elif client.event == EVENT_WS_CHUNK_LAST:
|
|
517
|
+
client.context['chunks'].append(client.ws_message)
|
|
518
|
+
# process all chunks...
|
|
519
|
+
client.ws_send('received')
|
|
520
|
+
|
|
521
|
+
elif client.event == EVENT_WS_PING:
|
|
522
|
+
pass # pong sent automatically
|
|
523
|
+
|
|
524
|
+
elif client.event == EVENT_WS_CLOSE:
|
|
525
|
+
print("WebSocket closed")
|
|
526
|
+
```
|
|
527
|
+
|
|
528
|
+
### Non-Event Mode (blocking, simple)
|
|
529
|
+
|
|
530
|
+
```python
|
|
531
|
+
from uhttp.server import HttpServer
|
|
532
|
+
|
|
533
|
+
server = HttpServer(port=8080)
|
|
534
|
+
|
|
535
|
+
while True:
|
|
536
|
+
client = server.wait()
|
|
537
|
+
if client and client.is_websocket_request:
|
|
538
|
+
ws = client.accept_websocket() # returns WebSocket object
|
|
539
|
+
while not ws.is_closed:
|
|
540
|
+
msg = ws.recv(timeout=5)
|
|
541
|
+
if msg is not None:
|
|
542
|
+
ws.send(msg) # echo
|
|
543
|
+
elif client:
|
|
544
|
+
client.respond("hello")
|
|
545
|
+
```
|
|
546
|
+
|
|
547
|
+
### WebSocket API
|
|
548
|
+
|
|
549
|
+
**HttpConnection properties:**
|
|
550
|
+
- `is_websocket_request` - True if request is a WebSocket upgrade
|
|
551
|
+
- `is_websocket` - True if connection is in WebSocket mode
|
|
552
|
+
- `ws_message` - Last received message (str for text, bytes for binary)
|
|
553
|
+
|
|
554
|
+
**HttpConnection methods (event mode):**
|
|
555
|
+
- `accept_websocket()` - Accept upgrade, switch to WebSocket mode
|
|
556
|
+
- `ws_send(data)` - Send message (str → text frame, bytes → binary frame)
|
|
557
|
+
- `ws_ping(data=b'')` - Send ping frame
|
|
558
|
+
- `ws_close(code=1000, reason='')` - Close WebSocket connection
|
|
559
|
+
|
|
560
|
+
**WebSocket object (non-event mode):**
|
|
561
|
+
- `recv(timeout=None)` - Receive message (blocking). Returns str/bytes/None
|
|
562
|
+
- `send(data)` - Send message (str → text frame, bytes → binary frame)
|
|
563
|
+
- `ping(data=b'')` - Send ping frame
|
|
564
|
+
- `close(code=1000, reason='')` - Close connection
|
|
565
|
+
- `is_closed` - True if connection is closed
|
|
566
|
+
|
|
567
|
+
### Large Message Chunking
|
|
568
|
+
|
|
569
|
+
Messages larger than `MAX_WS_MESSAGE_LENGTH` (default 64KB, configurable via `max_ws_message_length` kwarg) are delivered in chunks via `EVENT_WS_CHUNK_FIRST`, `EVENT_WS_CHUNK_NEXT`, `EVENT_WS_CHUNK_LAST` events. All chunk events contain data in `ws_message`.
|
|
570
|
+
|
|
571
|
+
In non-event mode, messages exceeding the limit close the connection with status 1009.
|
|
572
|
+
|
|
445
573
|
|
|
446
574
|
## Event Mode
|
|
447
575
|
|
|
@@ -530,16 +658,70 @@ server = uhttp.server.HttpServer(address='::1', port=80)
|
|
|
530
658
|
../.venv/bin/python -m unittest discover -v tests/
|
|
531
659
|
```
|
|
532
660
|
|
|
533
|
-
|
|
661
|
+
### MicroPython integration tests
|
|
662
|
+
|
|
663
|
+
Tests run HTTP server on real ESP32 hardware, with test client on PC.
|
|
664
|
+
Requires [mpytool](https://github.com/cortexm/mpytool) and `uhttp-client`.
|
|
665
|
+
|
|
666
|
+
**Configuration:**
|
|
667
|
+
|
|
668
|
+
1. WiFi credentials in `~/.config/uhttp/wifi.json`:
|
|
669
|
+
```json
|
|
670
|
+
{"ssid": "MyWiFi", "password": "secret"}
|
|
671
|
+
```
|
|
672
|
+
|
|
673
|
+
2. Serial port via environment variable or mpytool config:
|
|
674
|
+
```bash
|
|
675
|
+
# Environment variable
|
|
676
|
+
export MPY_TEST_PORT=/dev/ttyUSB0
|
|
677
|
+
|
|
678
|
+
# Or mpytool config
|
|
679
|
+
echo "/dev/ttyUSB0" > ~/.config/mpytool/ESP32
|
|
680
|
+
```
|
|
681
|
+
|
|
682
|
+
**Run tests:**
|
|
683
|
+
|
|
684
|
+
```bash
|
|
685
|
+
# Install dependencies
|
|
686
|
+
../.venv/bin/pip install uhttp-client mpytool
|
|
687
|
+
```
|
|
688
|
+
|
|
689
|
+
uhttp-client - for other integration tests
|
|
690
|
+
mpytool - for mpy_integration tests
|
|
691
|
+
|
|
692
|
+
```bash
|
|
693
|
+
# Run tests
|
|
694
|
+
MPY_TEST_PORT=/dev/ttyUSB0 ../.venv/bin/python -m unittest tests.test_mpy_integration -v
|
|
695
|
+
```
|
|
696
|
+
|
|
697
|
+
The tests upload `server.py` to ESP32, start HTTP server, and send requests from PC.
|
|
534
698
|
|
|
535
699
|
### CI
|
|
536
700
|
|
|
537
|
-
Tests run automatically on push/PR via GitHub Actions
|
|
701
|
+
Tests run automatically on push/PR via GitHub Actions:
|
|
702
|
+
- Unit tests: Ubuntu + Windows, Python 3.10 + 3.14
|
|
703
|
+
- MicroPython tests: Self-hosted runner with ESP32
|
|
704
|
+
|
|
538
705
|
|
|
706
|
+
## Expect: 100-continue
|
|
539
707
|
|
|
540
|
-
|
|
708
|
+
Server supports `Expect: 100-continue` header for large uploads:
|
|
541
709
|
|
|
542
|
-
-
|
|
543
|
-
|
|
544
|
-
|
|
545
|
-
|
|
710
|
+
**Non-event mode:** Server automatically sends `100 Continue` response when client sends `Expect: 100-continue` header, then waits for body.
|
|
711
|
+
|
|
712
|
+
**Event mode:** Server sends `100 Continue` only when application calls `accept_body()`. This allows rejecting uploads early (e.g., respond with 413 or 401) without accepting body data.
|
|
713
|
+
|
|
714
|
+
```python
|
|
715
|
+
from uhttp.server import HttpServer, EVENT_HEADERS
|
|
716
|
+
|
|
717
|
+
server = HttpServer(port=8080, event_mode=True)
|
|
718
|
+
|
|
719
|
+
while True:
|
|
720
|
+
client = server.wait()
|
|
721
|
+
if client and client.event == EVENT_HEADERS:
|
|
722
|
+
if client.content_length > MAX_ALLOWED:
|
|
723
|
+
# No 100 Continue sent - client won't upload
|
|
724
|
+
client.respond({'error': 'too large'}, status=413)
|
|
725
|
+
else:
|
|
726
|
+
client.accept_body() # Sends 100 Continue, starts receiving
|
|
727
|
+
```
|
|
@@ -12,6 +12,7 @@ Micro HTTP server for MicroPython and CPython.
|
|
|
12
12
|
- SSL/TLS for HTTPS connections
|
|
13
13
|
- IPv6 and dual-stack support
|
|
14
14
|
- Event mode for streaming large uploads
|
|
15
|
+
- WebSocket support (RFC 6455)
|
|
15
16
|
- Memory-efficient (~32KB RAM minimum)
|
|
16
17
|
|
|
17
18
|
## Installation
|
|
@@ -277,6 +278,10 @@ Parameters:
|
|
|
277
278
|
|
|
278
279
|
- Returns `True` if event mode is enabled
|
|
279
280
|
|
|
281
|
+
**`max_ws_message_length`** (kwarg)
|
|
282
|
+
|
|
283
|
+
- Maximum WebSocket message size before chunking (default: 64KB)
|
|
284
|
+
|
|
280
285
|
#### Methods:
|
|
281
286
|
|
|
282
287
|
**`event_write(self, sockets)`**
|
|
@@ -378,6 +383,18 @@ Parameters:
|
|
|
378
383
|
|
|
379
384
|
- Application storage attribute for request state (read-write)
|
|
380
385
|
|
|
386
|
+
**`is_websocket_request(self)`**
|
|
387
|
+
|
|
388
|
+
- True if request is a WebSocket upgrade request
|
|
389
|
+
|
|
390
|
+
**`is_websocket(self)`**
|
|
391
|
+
|
|
392
|
+
- True if connection is in WebSocket mode
|
|
393
|
+
|
|
394
|
+
**`ws_message(self)`**
|
|
395
|
+
|
|
396
|
+
- Last received WebSocket message (str for text frames, bytes for binary frames)
|
|
397
|
+
|
|
381
398
|
#### Methods:
|
|
382
399
|
|
|
383
400
|
**`headers_get(self, key, default=None)`**
|
|
@@ -425,6 +442,117 @@ Parameters:
|
|
|
425
442
|
- Read available data from buffer
|
|
426
443
|
- Returns: bytes or None if no data available
|
|
427
444
|
|
|
445
|
+
**`accept_websocket(self)`**
|
|
446
|
+
|
|
447
|
+
- Accept WebSocket upgrade. Event mode: switches to WS mode. Non-event mode: returns `WebSocket` object.
|
|
448
|
+
|
|
449
|
+
**`ws_send(self, data)`** (event mode, WebSocket)
|
|
450
|
+
|
|
451
|
+
- Send WebSocket message. `str` → text frame, `bytes` → binary frame.
|
|
452
|
+
|
|
453
|
+
**`ws_ping(self, data=b'')`** (event mode, WebSocket)
|
|
454
|
+
|
|
455
|
+
- Send WebSocket ping frame
|
|
456
|
+
|
|
457
|
+
**`ws_close(self, code=1000, reason='')`** (event mode, WebSocket)
|
|
458
|
+
|
|
459
|
+
- Send WebSocket close frame and close connection
|
|
460
|
+
|
|
461
|
+
|
|
462
|
+
## WebSocket Support
|
|
463
|
+
|
|
464
|
+
uHTTP supports WebSocket connections (RFC 6455) in both event mode and non-event mode.
|
|
465
|
+
|
|
466
|
+
### Event Mode (non-blocking, multiple connections)
|
|
467
|
+
|
|
468
|
+
```python
|
|
469
|
+
from uhttp.server import (
|
|
470
|
+
HttpServer, EVENT_REQUEST, EVENT_WS_REQUEST,
|
|
471
|
+
EVENT_WS_MESSAGE, EVENT_WS_CHUNK_FIRST,
|
|
472
|
+
EVENT_WS_CHUNK_NEXT, EVENT_WS_CHUNK_LAST,
|
|
473
|
+
EVENT_WS_PING, EVENT_WS_CLOSE
|
|
474
|
+
)
|
|
475
|
+
|
|
476
|
+
server = HttpServer(port=8080, event_mode=True)
|
|
477
|
+
|
|
478
|
+
while True:
|
|
479
|
+
client = server.wait()
|
|
480
|
+
if not client:
|
|
481
|
+
continue
|
|
482
|
+
|
|
483
|
+
if client.event == EVENT_WS_REQUEST:
|
|
484
|
+
client.accept_websocket()
|
|
485
|
+
|
|
486
|
+
elif client.event == EVENT_REQUEST:
|
|
487
|
+
client.respond({'status': 'ok'})
|
|
488
|
+
|
|
489
|
+
elif client.event == EVENT_WS_MESSAGE:
|
|
490
|
+
# str for text frames, bytes for binary frames
|
|
491
|
+
client.ws_send(client.ws_message) # echo
|
|
492
|
+
|
|
493
|
+
elif client.event == EVENT_WS_CHUNK_FIRST:
|
|
494
|
+
client.context = {'chunks': [client.ws_message]}
|
|
495
|
+
|
|
496
|
+
elif client.event == EVENT_WS_CHUNK_NEXT:
|
|
497
|
+
client.context['chunks'].append(client.ws_message)
|
|
498
|
+
|
|
499
|
+
elif client.event == EVENT_WS_CHUNK_LAST:
|
|
500
|
+
client.context['chunks'].append(client.ws_message)
|
|
501
|
+
# process all chunks...
|
|
502
|
+
client.ws_send('received')
|
|
503
|
+
|
|
504
|
+
elif client.event == EVENT_WS_PING:
|
|
505
|
+
pass # pong sent automatically
|
|
506
|
+
|
|
507
|
+
elif client.event == EVENT_WS_CLOSE:
|
|
508
|
+
print("WebSocket closed")
|
|
509
|
+
```
|
|
510
|
+
|
|
511
|
+
### Non-Event Mode (blocking, simple)
|
|
512
|
+
|
|
513
|
+
```python
|
|
514
|
+
from uhttp.server import HttpServer
|
|
515
|
+
|
|
516
|
+
server = HttpServer(port=8080)
|
|
517
|
+
|
|
518
|
+
while True:
|
|
519
|
+
client = server.wait()
|
|
520
|
+
if client and client.is_websocket_request:
|
|
521
|
+
ws = client.accept_websocket() # returns WebSocket object
|
|
522
|
+
while not ws.is_closed:
|
|
523
|
+
msg = ws.recv(timeout=5)
|
|
524
|
+
if msg is not None:
|
|
525
|
+
ws.send(msg) # echo
|
|
526
|
+
elif client:
|
|
527
|
+
client.respond("hello")
|
|
528
|
+
```
|
|
529
|
+
|
|
530
|
+
### WebSocket API
|
|
531
|
+
|
|
532
|
+
**HttpConnection properties:**
|
|
533
|
+
- `is_websocket_request` - True if request is a WebSocket upgrade
|
|
534
|
+
- `is_websocket` - True if connection is in WebSocket mode
|
|
535
|
+
- `ws_message` - Last received message (str for text, bytes for binary)
|
|
536
|
+
|
|
537
|
+
**HttpConnection methods (event mode):**
|
|
538
|
+
- `accept_websocket()` - Accept upgrade, switch to WebSocket mode
|
|
539
|
+
- `ws_send(data)` - Send message (str → text frame, bytes → binary frame)
|
|
540
|
+
- `ws_ping(data=b'')` - Send ping frame
|
|
541
|
+
- `ws_close(code=1000, reason='')` - Close WebSocket connection
|
|
542
|
+
|
|
543
|
+
**WebSocket object (non-event mode):**
|
|
544
|
+
- `recv(timeout=None)` - Receive message (blocking). Returns str/bytes/None
|
|
545
|
+
- `send(data)` - Send message (str → text frame, bytes → binary frame)
|
|
546
|
+
- `ping(data=b'')` - Send ping frame
|
|
547
|
+
- `close(code=1000, reason='')` - Close connection
|
|
548
|
+
- `is_closed` - True if connection is closed
|
|
549
|
+
|
|
550
|
+
### Large Message Chunking
|
|
551
|
+
|
|
552
|
+
Messages larger than `MAX_WS_MESSAGE_LENGTH` (default 64KB, configurable via `max_ws_message_length` kwarg) are delivered in chunks via `EVENT_WS_CHUNK_FIRST`, `EVENT_WS_CHUNK_NEXT`, `EVENT_WS_CHUNK_LAST` events. All chunk events contain data in `ws_message`.
|
|
553
|
+
|
|
554
|
+
In non-event mode, messages exceeding the limit close the connection with status 1009.
|
|
555
|
+
|
|
428
556
|
|
|
429
557
|
## Event Mode
|
|
430
558
|
|
|
@@ -513,16 +641,70 @@ server = uhttp.server.HttpServer(address='::1', port=80)
|
|
|
513
641
|
../.venv/bin/python -m unittest discover -v tests/
|
|
514
642
|
```
|
|
515
643
|
|
|
516
|
-
|
|
644
|
+
### MicroPython integration tests
|
|
645
|
+
|
|
646
|
+
Tests run HTTP server on real ESP32 hardware, with test client on PC.
|
|
647
|
+
Requires [mpytool](https://github.com/cortexm/mpytool) and `uhttp-client`.
|
|
648
|
+
|
|
649
|
+
**Configuration:**
|
|
650
|
+
|
|
651
|
+
1. WiFi credentials in `~/.config/uhttp/wifi.json`:
|
|
652
|
+
```json
|
|
653
|
+
{"ssid": "MyWiFi", "password": "secret"}
|
|
654
|
+
```
|
|
655
|
+
|
|
656
|
+
2. Serial port via environment variable or mpytool config:
|
|
657
|
+
```bash
|
|
658
|
+
# Environment variable
|
|
659
|
+
export MPY_TEST_PORT=/dev/ttyUSB0
|
|
660
|
+
|
|
661
|
+
# Or mpytool config
|
|
662
|
+
echo "/dev/ttyUSB0" > ~/.config/mpytool/ESP32
|
|
663
|
+
```
|
|
664
|
+
|
|
665
|
+
**Run tests:**
|
|
666
|
+
|
|
667
|
+
```bash
|
|
668
|
+
# Install dependencies
|
|
669
|
+
../.venv/bin/pip install uhttp-client mpytool
|
|
670
|
+
```
|
|
671
|
+
|
|
672
|
+
uhttp-client - for other integration tests
|
|
673
|
+
mpytool - for mpy_integration tests
|
|
674
|
+
|
|
675
|
+
```bash
|
|
676
|
+
# Run tests
|
|
677
|
+
MPY_TEST_PORT=/dev/ttyUSB0 ../.venv/bin/python -m unittest tests.test_mpy_integration -v
|
|
678
|
+
```
|
|
679
|
+
|
|
680
|
+
The tests upload `server.py` to ESP32, start HTTP server, and send requests from PC.
|
|
517
681
|
|
|
518
682
|
### CI
|
|
519
683
|
|
|
520
|
-
Tests run automatically on push/PR via GitHub Actions
|
|
684
|
+
Tests run automatically on push/PR via GitHub Actions:
|
|
685
|
+
- Unit tests: Ubuntu + Windows, Python 3.10 + 3.14
|
|
686
|
+
- MicroPython tests: Self-hosted runner with ESP32
|
|
687
|
+
|
|
521
688
|
|
|
689
|
+
## Expect: 100-continue
|
|
522
690
|
|
|
523
|
-
|
|
691
|
+
Server supports `Expect: 100-continue` header for large uploads:
|
|
524
692
|
|
|
525
|
-
-
|
|
526
|
-
|
|
527
|
-
|
|
528
|
-
|
|
693
|
+
**Non-event mode:** Server automatically sends `100 Continue` response when client sends `Expect: 100-continue` header, then waits for body.
|
|
694
|
+
|
|
695
|
+
**Event mode:** Server sends `100 Continue` only when application calls `accept_body()`. This allows rejecting uploads early (e.g., respond with 413 or 401) without accepting body data.
|
|
696
|
+
|
|
697
|
+
```python
|
|
698
|
+
from uhttp.server import HttpServer, EVENT_HEADERS
|
|
699
|
+
|
|
700
|
+
server = HttpServer(port=8080, event_mode=True)
|
|
701
|
+
|
|
702
|
+
while True:
|
|
703
|
+
client = server.wait()
|
|
704
|
+
if client and client.event == EVENT_HEADERS:
|
|
705
|
+
if client.content_length > MAX_ALLOWED:
|
|
706
|
+
# No 100 Continue sent - client won't upload
|
|
707
|
+
client.respond({'error': 'too large'}, status=413)
|
|
708
|
+
else:
|
|
709
|
+
client.accept_body() # Sends 100 Continue, starts receiving
|
|
710
|
+
```
|