diff --git a/README.md b/README.md index e19127d..50f5be0 100644 --- a/README.md +++ b/README.md @@ -236,10 +236,11 @@ For ASGI servers: - [Uvicorn](https://www.uvicorn.org/) - Lightning-fast ASGI server - [Daphne](https://github.com/django/daphne) - Django Channels' ASGI server with HTTP/2 support - [Hypercorn](https://gitlab.com/pgjones/hypercorn) - ASGI server with HTTP/2 and HTTP/3 support +- [Gunicorn](https://gunicorn.org/) - Python WSGI and ASGI HTTP Server For WSGI servers: -- [Gunicorn](https://gunicorn.org/) - Python WSGI HTTP Server +- [Gunicorn](https://gunicorn.org/) - Python WSGI and ASGI HTTP Server - [uWSGI](https://uwsgi-docs.readthedocs.io/) - Full-featured application server - Any WSGI-compliant server diff --git a/conformance/test/server.py b/conformance/test/server.py index ef39ecc..27f28ae 100644 --- a/conformance/test/server.py +++ b/conformance/test/server.py @@ -543,18 +543,17 @@ async def serve_granian( async def serve_gunicorn( request: ServerCompatRequest, + mode: Mode, certfile: str | None, keyfile: str | None, cafile: str | None, port_future: asyncio.Future[int], ): - args = [ - "--bind=127.0.0.1:0", - "--workers=4", - "--worker-class=gthread", - "--threads=16", - "--keep-alive=0", - ] + args = ["--bind=127.0.0.1:0", "--workers=4"] + if mode == "sync": + args.extend(["--worker-class=gthread", "--threads=16", "--keep-alive=0"]) + else: + args.append("--worker-class=asgi") if certfile: args.append(f"--certfile={certfile}") if keyfile: @@ -563,7 +562,7 @@ async def serve_gunicorn( args.append(f"--ca-certs={cafile}") args.append(f"--cert-reqs={ssl.CERT_REQUIRED}") - args.append("server:wsgi_app") + args.append("server:wsgi_app" if mode == "sync" else "server:asgi_app") proc = await asyncio.create_subprocess_exec( "gunicorn", @@ -791,11 +790,10 @@ async def main() -> None: ) ) case "gunicorn": - if args.mode == "async": - msg = "gunicorn does not support async mode" - raise ValueError(msg) serve_task = asyncio.create_task( - serve_gunicorn(request, certfile, keyfile, cafile, port_future) + serve_gunicorn( + request, args.mode, certfile, keyfile, cafile, port_future + ) ) case "hypercorn": serve_task = asyncio.create_task( diff --git a/conformance/test/test_server.py b/conformance/test/test_server.py index a38ac35..0d66c43 100644 --- a/conformance/test/test_server.py +++ b/conformance/test/test_server.py @@ -42,6 +42,8 @@ def macos_raise_ulimit(): "Server Message Size/HTTPVersion:1/**/first-request-exceeds-server-limit", ] +_skip_http2_http3 = ["--skip", "**/HTTPVersion:2/**", "--skip", "**/HTTPVersion:3/**"] + @pytest.mark.parametrize("server", ["gunicorn", "pyvoy"]) def test_server_sync(server: str, cov: Coverage) -> None: @@ -52,7 +54,7 @@ def test_server_sync(server: str, cov: Coverage) -> None: match server: case "gunicorn": # gunicorn doesn't support HTTP/2 or 3 - opts = ["--skip", "**/HTTPVersion:2/**", "--skip", "**/HTTPVersion:3/**"] + opts = _skip_http2_http3 result = subprocess.run( [ @@ -77,7 +79,7 @@ def test_server_sync(server: str, cov: Coverage) -> None: pytest.fail(f"\n{result.stdout}\n{result.stderr}") -@pytest.mark.parametrize("server", ["daphne", "pyvoy", "uvicorn"]) +@pytest.mark.parametrize("server", ["daphne", "gunicorn", "pyvoy", "uvicorn"]) def test_server_async(server: str, cov: Coverage) -> None: args = maybe_patch_args_with_debug( [sys.executable, _server_py_path, "--mode", "async", "--server", server] @@ -104,9 +106,24 @@ def test_server_async(server: str, cov: Coverage) -> None: "--skip", "gRPC Unexpected Requests/**", ] + case "gunicorn": + opts = [ + # gunicorn's native HTTP/2 ASGI worker sends GOAWAY after very few streams + # under load; no upstream issue filed yet (see PR #3568 for related h2 fix) + *_skip_http2_http3, + # gunicorn's gunicorn_h1c C parser returns 400 "Invalid request line" for + # gRPC-Web requests broadly; no upstream issue filed yet + # (see issue #3563 for a related gunicorn_h1c strictness regression) + "--skip", + "**/Protocol:PROTOCOL_GRPC_WEB/**", + "--skip", + "gRPC-Web Proto Sub-Format Requests/**", + "--skip", + "gRPC-Web Unexpected Requests/**", + ] case "uvicorn": # uvicorn doesn't support HTTP/2 or 3 - opts = ["--skip", "**/HTTPVersion:2/**", "--skip", "**/HTTPVersion:3/**"] + opts = _skip_http2_http3 result = subprocess.run( [ "go",