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.
Files changed (42) hide show
  1. uhttp_server-2.3.0/.github/workflows/micropython.yml +40 -0
  2. uhttp_server-2.3.0/.github/workflows/publish.yml +35 -0
  3. uhttp_server-2.3.0/.github/workflows/tests.yml +30 -0
  4. uhttp_server-2.3.0/.gitignore +10 -0
  5. {uhttp_server-2.2.1 → uhttp_server-2.3.0}/PKG-INFO +190 -8
  6. {uhttp_server-2.2.1 → uhttp_server-2.3.0}/README.md +189 -7
  7. uhttp_server-2.3.0/examples/client_with_server.py +123 -0
  8. uhttp_server-2.3.0/examples/http_to_https_redirect.py +85 -0
  9. uhttp_server-2.3.0/examples/https_server.py +57 -0
  10. {uhttp_server-2.2.1 → uhttp_server-2.3.0}/pyproject.toml +5 -2
  11. uhttp_server-2.3.0/tests/__init__.py +0 -0
  12. uhttp_server-2.3.0/tests/test_mpy_integration.py +330 -0
  13. uhttp_server-2.3.0/tests/test_mpy_websocket.py +488 -0
  14. uhttp_server-2.3.0/tests/test_websocket.py +817 -0
  15. uhttp_server-2.3.0/tests/test_websocket_utils.py +343 -0
  16. {uhttp_server-2.2.1 → uhttp_server-2.3.0}/uhttp/server.py +540 -6
  17. {uhttp_server-2.2.1 → uhttp_server-2.3.0}/uhttp_server.egg-info/PKG-INFO +190 -8
  18. {uhttp_server-2.2.1 → uhttp_server-2.3.0}/uhttp_server.egg-info/SOURCES.txt +12 -0
  19. {uhttp_server-2.2.1 → uhttp_server-2.3.0}/LICENSE +0 -0
  20. {uhttp_server-2.2.1 → uhttp_server-2.3.0}/setup.cfg +0 -0
  21. {uhttp_server-2.2.1 → uhttp_server-2.3.0}/tests/test_100_continue.py +0 -0
  22. {uhttp_server-2.2.1 → uhttp_server-2.3.0}/tests/test_concurrent_connections.py +0 -0
  23. {uhttp_server-2.2.1 → uhttp_server-2.3.0}/tests/test_content_length_security.py +0 -0
  24. {uhttp_server-2.2.1 → uhttp_server-2.3.0}/tests/test_data_parsing.py +0 -0
  25. {uhttp_server-2.2.1 → uhttp_server-2.3.0}/tests/test_double_response.py +0 -0
  26. {uhttp_server-2.2.1 → uhttp_server-2.3.0}/tests/test_eagain.py +0 -0
  27. {uhttp_server-2.2.1 → uhttp_server-2.3.0}/tests/test_error_handling.py +0 -0
  28. {uhttp_server-2.2.1 → uhttp_server-2.3.0}/tests/test_event_mode.py +0 -0
  29. {uhttp_server-2.2.1 → uhttp_server-2.3.0}/tests/test_ipv6.py +0 -0
  30. {uhttp_server-2.2.1 → uhttp_server-2.3.0}/tests/test_keepalive.py +0 -0
  31. {uhttp_server-2.2.1 → uhttp_server-2.3.0}/tests/test_keepalive_basic.py +0 -0
  32. {uhttp_server-2.2.1 → uhttp_server-2.3.0}/tests/test_keepalive_http10.py +0 -0
  33. {uhttp_server-2.2.1 → uhttp_server-2.3.0}/tests/test_keepalive_limits.py +0 -0
  34. {uhttp_server-2.2.1 → uhttp_server-2.3.0}/tests/test_keepalive_simple.py +0 -0
  35. {uhttp_server-2.2.1 → uhttp_server-2.3.0}/tests/test_multipart.py +0 -0
  36. {uhttp_server-2.2.1 → uhttp_server-2.3.0}/tests/test_pipelining.py +0 -0
  37. {uhttp_server-2.2.1 → uhttp_server-2.3.0}/tests/test_respond_file.py +0 -0
  38. {uhttp_server-2.2.1 → uhttp_server-2.3.0}/tests/test_respond_file_race.py +0 -0
  39. {uhttp_server-2.2.1 → uhttp_server-2.3.0}/tests/test_ssl.py +0 -0
  40. {uhttp_server-2.2.1 → uhttp_server-2.3.0}/tests/test_utils.py +0 -0
  41. {uhttp_server-2.2.1 → uhttp_server-2.3.0}/uhttp_server.egg-info/dependency_links.txt +0 -0
  42. {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/
@@ -0,0 +1,10 @@
1
+ *.egg-info
2
+ __pycache__
3
+ *.pyc
4
+ .*
5
+ !.gitignore
6
+ !.github
7
+ !.mpyproject
8
+ *.pem
9
+ *.md
10
+ !README*.md
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: uhttp-server
3
- Version: 2.2.1
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
- For running tests from meta-repo, see [uhttp README](https://github.com/pavelrevak/uhttp#testing).
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 (Ubuntu + Windows, Python 3.10 + 3.14).
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
- ## TODO
708
+ Server supports `Expect: 100-continue` header for large uploads:
541
709
 
542
- - Cookie attributes support (Path, Domain, Secure, HttpOnly, SameSite, Expires)
543
- - Expect: 100-continue support - currently causes deadlock (client waits for 100, server waits for body)
544
- - Streaming API for sending large responses (handle EAGAIN)
545
- - Chunked transfer encoding support (receiving and sending)
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
- For running tests from meta-repo, see [uhttp README](https://github.com/pavelrevak/uhttp#testing).
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 (Ubuntu + Windows, Python 3.10 + 3.14).
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
- ## TODO
691
+ Server supports `Expect: 100-continue` header for large uploads:
524
692
 
525
- - Cookie attributes support (Path, Domain, Secure, HttpOnly, SameSite, Expires)
526
- - Expect: 100-continue support - currently causes deadlock (client waits for 100, server waits for body)
527
- - Streaming API for sending large responses (handle EAGAIN)
528
- - Chunked transfer encoding support (receiving and sending)
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
+ ```