Skip to content

Commit 6aba74a

Browse files
committed
feat(config): add lenient flags for httptools
- Add HTTPTOOLS_LENIENT type and related configuration options - Implement lenient flags support in HttpToolsProtocol - Update documentation and CLI options for lenient flags - Add test case for lenient_data_after_close flag
1 parent ed9902d commit 6aba74a

File tree

6 files changed

+56
-4
lines changed

6 files changed

+56
-4
lines changed

docs/deployment.md

+3
Original file line numberDiff line numberDiff line change
@@ -135,6 +135,9 @@ Options:
135135
buffer of an incomplete event.
136136
--factory Treat APP as an application factory, i.e. a
137137
() -> <ASGI app> callable.
138+
--lenient-flag [lenient_headers|lenient_chunked_length|lenient_keep_alive|lenient_transfer_encoding|lenient_version|lenient_data_after_close|lenient_optional_lf_after_cr|lenient_optional_cr_before_lf|lenient_optional_crlf_after_chunk|lenient_spaces_after_chunk_size]
139+
Lenient flags for httptools only,
140+
Details:https://llhttp.org/
138141
--help Show this message and exit.
139142
```
140143

docs/index.md

+3
Original file line numberDiff line numberDiff line change
@@ -205,6 +205,9 @@ Options:
205205
buffer of an incomplete event.
206206
--factory Treat APP as an application factory, i.e. a
207207
() -> <ASGI app> callable.
208+
--lenient-flag [lenient_headers|lenient_chunked_length|lenient_keep_alive|lenient_transfer_encoding|lenient_version|lenient_data_after_close|lenient_optional_lf_after_cr|lenient_optional_cr_before_lf|lenient_optional_crlf_after_chunk|lenient_spaces_after_chunk_size]
209+
Lenient flags for httptools only,
210+
Details:https://llhttp.org/
208211
--help Show this message and exit.
209212
```
210213

tests/protocols/test_http.py

+4-2
Original file line numberDiff line numberDiff line change
@@ -1023,8 +1023,10 @@ async def test_return_close_header(http_protocol_cls: HTTPProtocol):
10231023

10241024
async def test_close_connection_with_multiple_requests(http_protocol_cls: HTTPProtocol):
10251025
app = Response("Hello, world", media_type="text/plain")
1026-
1027-
protocol = get_connected_protocol(app, http_protocol_cls)
1026+
config_kw = {}
1027+
if http_protocol_cls == HttpToolsProtocol:
1028+
config_kw = {"lenient_flags": ["lenient_data_after_close"]}
1029+
protocol = get_connected_protocol(app, http_protocol_cls, **config_kw)
10281030
protocol.data_received(REQUEST_AFTER_CONNECTION_CLOSE)
10291031
await protocol.loop.run_one()
10301032
assert b"HTTP/1.1 200 OK" in protocol.transport.buffer

uvicorn/config.py

+27-1
Original file line numberDiff line numberDiff line change
@@ -28,7 +28,31 @@
2828
LifespanType = Literal["auto", "on", "off"]
2929
LoopSetupType = Literal["none", "auto", "asyncio", "uvloop"]
3030
InterfaceType = Literal["auto", "asgi3", "asgi2", "wsgi"]
31-
31+
HTTPToolsLenientType = Literal[
32+
"lenient_headers",
33+
"lenient_chunked_length",
34+
"lenient_keep_alive",
35+
"lenient_transfer_encoding",
36+
"lenient_version",
37+
"lenient_data_after_close",
38+
"lenient_optional_lf_after_cr",
39+
"lenient_optional_cr_before_lf",
40+
"lenient_optional_crlf_after_chunk",
41+
"lenient_spaces_after_chunk_size",
42+
]
43+
44+
HTTPTOOLS_LENIENT: list[HTTPToolsLenientType] = [
45+
"lenient_headers",
46+
"lenient_chunked_length",
47+
"lenient_keep_alive",
48+
"lenient_transfer_encoding",
49+
"lenient_version",
50+
"lenient_data_after_close",
51+
"lenient_optional_lf_after_cr",
52+
"lenient_optional_cr_before_lf",
53+
"lenient_optional_crlf_after_chunk",
54+
"lenient_spaces_after_chunk_size",
55+
]
3256
LOG_LEVELS: dict[str, int] = {
3357
"critical": logging.CRITICAL,
3458
"error": logging.ERROR,
@@ -223,6 +247,7 @@ def __init__(
223247
headers: list[tuple[str, str]] | None = None,
224248
factory: bool = False,
225249
h11_max_incomplete_event_size: int | None = None,
250+
lenient_flags: HTTPToolsLenientType | None = None,
226251
):
227252
self.app = app
228253
self.host = host
@@ -268,6 +293,7 @@ def __init__(
268293
self.encoded_headers: list[tuple[bytes, bytes]] = []
269294
self.factory = factory
270295
self.h11_max_incomplete_event_size = h11_max_incomplete_event_size
296+
self.lenient_flags = lenient_flags
271297

272298
self.loaded = False
273299
self.configure_logging()

uvicorn/main.py

+16
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,7 @@
1515
from uvicorn._types import ASGIApplication
1616
from uvicorn.config import (
1717
HTTP_PROTOCOLS,
18+
HTTPTOOLS_LENIENT,
1819
INTERFACES,
1920
LIFESPAN,
2021
LOG_LEVELS,
@@ -24,6 +25,7 @@
2425
WS_PROTOCOLS,
2526
Config,
2627
HTTPProtocolType,
28+
HTTPToolsLenientType,
2729
InterfaceType,
2830
LifespanType,
2931
LoopSetupType,
@@ -38,6 +40,7 @@
3840
LIFESPAN_CHOICES = click.Choice(list(LIFESPAN.keys()))
3941
LOOP_CHOICES = click.Choice([key for key in LOOP_SETUPS.keys() if key != "none"])
4042
INTERFACE_CHOICES = click.Choice(INTERFACES)
43+
HTTPTOOLS_CHOICES = click.Choice(HTTPTOOLS_LENIENT)
4144

4245
STARTUP_FAILURE = 3
4346

@@ -360,6 +363,15 @@ def print_version(ctx: click.Context, param: click.Parameter, value: bool) -> No
360363
help="Treat APP as an application factory, i.e. a () -> <ASGI app> callable.",
361364
show_default=True,
362365
)
366+
@click.option(
367+
"--lenient-flag",
368+
"lenient_flags",
369+
type=HTTPTOOLS_CHOICES,
370+
multiple=True,
371+
default=None,
372+
help="Lenient flags for httptools only, Details:https://llhttp.org/",
373+
show_default=True,
374+
)
363375
def main(
364376
app: str,
365377
host: str,
@@ -408,6 +420,7 @@ def main(
408420
app_dir: str,
409421
h11_max_incomplete_event_size: int | None,
410422
factory: bool,
423+
lenient_flags: HTTPToolsLenientType | None,
411424
) -> None:
412425
run(
413426
app,
@@ -457,6 +470,7 @@ def main(
457470
factory=factory,
458471
app_dir=app_dir,
459472
h11_max_incomplete_event_size=h11_max_incomplete_event_size,
473+
lenient_flags=lenient_flags,
460474
)
461475

462476

@@ -509,6 +523,7 @@ def run(
509523
app_dir: str | None = None,
510524
factory: bool = False,
511525
h11_max_incomplete_event_size: int | None = None,
526+
lenient_flags: HTTPToolsLenientType | None = None,
512527
) -> None:
513528
if app_dir is not None:
514529
sys.path.insert(0, app_dir)
@@ -560,6 +575,7 @@ def run(
560575
use_colors=use_colors,
561576
factory=factory,
562577
h11_max_incomplete_event_size=h11_max_incomplete_event_size,
578+
lenient_flags=lenient_flags,
563579
)
564580
server = Server(config=config)
565581

uvicorn/protocols/http/httptools_impl.py

+3-1
Original file line numberDiff line numberDiff line change
@@ -58,7 +58,9 @@ def __init__(
5858
self.access_logger = logging.getLogger("uvicorn.access")
5959
self.access_log = self.access_logger.hasHandlers()
6060
self.parser = httptools.HttpRequestParser(self)
61-
self.parser.set_dangerous_leniencies(lenient_data_after_close=True)
61+
lenient_flags = config.lenient_flags
62+
if lenient_flags:
63+
self.parser.set_dangerous_leniencies(**{flag: True for flag in lenient_flags})
6264
self.ws_protocol_class = config.ws_protocol_class
6365
self.root_path = config.root_path
6466
self.limit_concurrency = config.limit_concurrency

0 commit comments

Comments
 (0)