mirror of
https://github.com/frappe/gunicorn.git
synced 2026-07-01 18:21:30 +08:00
Wire HttpParser to ASGI hot path, replacing AsyncRequest.parse() with direct buffer-based parsing. Add FastAsyncRequest wrapper for body reading. Replace per-request Queue/Task with BodyReceiver for on-demand body reading. Keep headers as bytes end-to-end to avoid conversion overhead. Add backpressure control and keepalive timer. Cache response status lines and Date header. Benchmark shows 3x improvement: ~875K req/s for simple GET (was ~340K).
305 lines
8.8 KiB
Python
305 lines
8.8 KiB
Python
#
|
|
# This file is part of gunicorn released under the MIT license.
|
|
# See the NOTICE for more information.
|
|
|
|
"""
|
|
Tests for ASGI worker components.
|
|
"""
|
|
|
|
import asyncio
|
|
import ipaddress
|
|
import pytest
|
|
|
|
from gunicorn.asgi.unreader import AsyncUnreader
|
|
from gunicorn.asgi.message import AsyncRequest
|
|
|
|
|
|
class MockStreamReader:
|
|
"""Mock asyncio.StreamReader for testing."""
|
|
|
|
def __init__(self, data):
|
|
self.data = data
|
|
self.pos = 0
|
|
|
|
async def read(self, size=-1):
|
|
if self.pos >= len(self.data):
|
|
return b""
|
|
if size < 0:
|
|
result = self.data[self.pos:]
|
|
self.pos = len(self.data)
|
|
else:
|
|
result = self.data[self.pos:self.pos + size]
|
|
self.pos += size
|
|
return result
|
|
|
|
async def readexactly(self, n):
|
|
if self.pos + n > len(self.data):
|
|
raise asyncio.IncompleteReadError(
|
|
self.data[self.pos:], n
|
|
)
|
|
result = self.data[self.pos:self.pos + n]
|
|
self.pos += n
|
|
return result
|
|
|
|
|
|
class MockConfig:
|
|
"""Mock gunicorn config for testing."""
|
|
|
|
def __init__(self):
|
|
self.is_ssl = False
|
|
self.proxy_protocol = "off"
|
|
self.proxy_allow_ips = ["127.0.0.1"]
|
|
self.forwarded_allow_ips = ["127.0.0.1"]
|
|
self._proxy_allow_networks = None
|
|
self._forwarded_allow_networks = None
|
|
self.secure_scheme_headers = {}
|
|
self.forwarder_headers = []
|
|
self.limit_request_line = 8190
|
|
self.limit_request_fields = 100
|
|
self.limit_request_field_size = 8190
|
|
self.permit_unconventional_http_method = False
|
|
self.permit_unconventional_http_version = False
|
|
self.permit_obsolete_folding = False
|
|
self.casefold_http_method = False
|
|
self.strip_header_spaces = False
|
|
self.header_map = "refuse"
|
|
|
|
def forwarded_allow_networks(self):
|
|
if self._forwarded_allow_networks is None:
|
|
self._forwarded_allow_networks = [
|
|
ipaddress.ip_network(addr)
|
|
for addr in self.forwarded_allow_ips
|
|
if addr != "*"
|
|
]
|
|
return self._forwarded_allow_networks
|
|
|
|
def proxy_allow_networks(self):
|
|
if self._proxy_allow_networks is None:
|
|
self._proxy_allow_networks = [
|
|
ipaddress.ip_network(addr)
|
|
for addr in self.proxy_allow_ips
|
|
if addr != "*"
|
|
]
|
|
return self._proxy_allow_networks
|
|
|
|
|
|
# AsyncUnreader Tests
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_async_unreader_read_chunk():
|
|
"""Test basic chunk reading."""
|
|
reader = MockStreamReader(b"hello world")
|
|
unreader = AsyncUnreader(reader)
|
|
data = await unreader.read()
|
|
assert data == b"hello world"
|
|
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_async_unreader_read_size():
|
|
"""Test reading specific size."""
|
|
reader = MockStreamReader(b"hello world")
|
|
unreader = AsyncUnreader(reader)
|
|
data = await unreader.read(5)
|
|
assert data == b"hello"
|
|
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_async_unreader_unread():
|
|
"""Test unread functionality."""
|
|
reader = MockStreamReader(b"hello world")
|
|
unreader = AsyncUnreader(reader)
|
|
|
|
# Read all data
|
|
data = await unreader.read()
|
|
assert data == b"hello world"
|
|
|
|
# Unread some data
|
|
unreader.unread(b"world")
|
|
|
|
# Read again should get unread data
|
|
data = await unreader.read()
|
|
assert data == b"world"
|
|
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_async_unreader_read_zero():
|
|
"""Test reading zero bytes."""
|
|
reader = MockStreamReader(b"hello")
|
|
unreader = AsyncUnreader(reader)
|
|
data = await unreader.read(0)
|
|
assert data == b""
|
|
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_async_unreader_read_empty():
|
|
"""Test reading from empty stream."""
|
|
reader = MockStreamReader(b"")
|
|
unreader = AsyncUnreader(reader)
|
|
data = await unreader.read()
|
|
assert data == b""
|
|
|
|
|
|
# AsyncRequest Tests
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_async_request_simple_get():
|
|
"""Test parsing a simple GET request."""
|
|
request_data = b"GET /path HTTP/1.1\r\nHost: localhost\r\n\r\n"
|
|
reader = MockStreamReader(request_data)
|
|
unreader = AsyncUnreader(reader)
|
|
cfg = MockConfig()
|
|
|
|
request = await AsyncRequest.parse(cfg, unreader, ("127.0.0.1", 8000))
|
|
|
|
assert request.method == "GET"
|
|
assert request.path == "/path"
|
|
assert request.version == (1, 1)
|
|
assert ("HOST", "localhost") in request.headers
|
|
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_async_request_with_query():
|
|
"""Test parsing request with query string."""
|
|
request_data = b"GET /search?q=test&page=1 HTTP/1.1\r\nHost: localhost\r\n\r\n"
|
|
reader = MockStreamReader(request_data)
|
|
unreader = AsyncUnreader(reader)
|
|
cfg = MockConfig()
|
|
|
|
request = await AsyncRequest.parse(cfg, unreader, ("127.0.0.1", 8000))
|
|
|
|
assert request.method == "GET"
|
|
assert request.path == "/search"
|
|
assert request.query == "q=test&page=1"
|
|
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_async_request_post_with_body():
|
|
"""Test parsing POST request with body."""
|
|
request_data = (
|
|
b"POST /submit HTTP/1.1\r\n"
|
|
b"Host: localhost\r\n"
|
|
b"Content-Length: 11\r\n"
|
|
b"\r\n"
|
|
b"hello=world"
|
|
)
|
|
reader = MockStreamReader(request_data)
|
|
unreader = AsyncUnreader(reader)
|
|
cfg = MockConfig()
|
|
|
|
request = await AsyncRequest.parse(cfg, unreader, ("127.0.0.1", 8000))
|
|
|
|
assert request.method == "POST"
|
|
assert request.path == "/submit"
|
|
assert request.content_length == 11
|
|
|
|
# Read body
|
|
body = await request.read_body(100)
|
|
assert body == b"hello=world"
|
|
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_async_request_multiple_headers():
|
|
"""Test parsing request with multiple headers."""
|
|
request_data = (
|
|
b"GET / HTTP/1.1\r\n"
|
|
b"Host: localhost\r\n"
|
|
b"Accept: text/html\r\n"
|
|
b"Accept-Language: en-US\r\n"
|
|
b"Connection: keep-alive\r\n"
|
|
b"\r\n"
|
|
)
|
|
reader = MockStreamReader(request_data)
|
|
unreader = AsyncUnreader(reader)
|
|
cfg = MockConfig()
|
|
|
|
request = await AsyncRequest.parse(cfg, unreader, ("127.0.0.1", 8000))
|
|
|
|
assert len(request.headers) == 4
|
|
assert request.get_header("HOST") == "localhost"
|
|
assert request.get_header("ACCEPT") == "text/html"
|
|
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_async_request_should_close_http10():
|
|
"""Test connection close detection for HTTP/1.0."""
|
|
request_data = b"GET / HTTP/1.0\r\nHost: localhost\r\n\r\n"
|
|
reader = MockStreamReader(request_data)
|
|
unreader = AsyncUnreader(reader)
|
|
cfg = MockConfig()
|
|
|
|
request = await AsyncRequest.parse(cfg, unreader, ("127.0.0.1", 8000))
|
|
|
|
assert request.version == (1, 0)
|
|
assert request.should_close() is True
|
|
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_async_request_should_close_connection_header():
|
|
"""Test connection close detection with Connection header."""
|
|
request_data = b"GET / HTTP/1.1\r\nHost: localhost\r\nConnection: close\r\n\r\n"
|
|
reader = MockStreamReader(request_data)
|
|
unreader = AsyncUnreader(reader)
|
|
cfg = MockConfig()
|
|
|
|
request = await AsyncRequest.parse(cfg, unreader, ("127.0.0.1", 8000))
|
|
|
|
assert request.should_close() is True
|
|
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_async_request_keepalive():
|
|
"""Test keepalive detection."""
|
|
request_data = b"GET / HTTP/1.1\r\nHost: localhost\r\nConnection: keep-alive\r\n\r\n"
|
|
reader = MockStreamReader(request_data)
|
|
unreader = AsyncUnreader(reader)
|
|
cfg = MockConfig()
|
|
|
|
request = await AsyncRequest.parse(cfg, unreader, ("127.0.0.1", 8000))
|
|
|
|
assert request.should_close() is False
|
|
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_async_request_no_body_for_get():
|
|
"""Test that GET requests have no body by default."""
|
|
request_data = b"GET / HTTP/1.1\r\nHost: localhost\r\n\r\n"
|
|
reader = MockStreamReader(request_data)
|
|
unreader = AsyncUnreader(reader)
|
|
cfg = MockConfig()
|
|
|
|
request = await AsyncRequest.parse(cfg, unreader, ("127.0.0.1", 8000))
|
|
|
|
assert request.content_length == 0
|
|
body = await request.read_body()
|
|
assert body == b""
|
|
|
|
|
|
# Error handling tests
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_async_request_invalid_method():
|
|
"""Test invalid HTTP method detection."""
|
|
from gunicorn.http.errors import InvalidRequestMethod
|
|
|
|
request_data = b"ge!t / HTTP/1.1\r\nHost: localhost\r\n\r\n"
|
|
reader = MockStreamReader(request_data)
|
|
unreader = AsyncUnreader(reader)
|
|
cfg = MockConfig()
|
|
|
|
with pytest.raises(InvalidRequestMethod):
|
|
await AsyncRequest.parse(cfg, unreader, ("127.0.0.1", 8000))
|
|
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_async_request_invalid_http_version():
|
|
"""Test invalid HTTP version detection."""
|
|
from gunicorn.http.errors import InvalidHTTPVersion
|
|
|
|
request_data = b"GET / HTTP/2.0\r\nHost: localhost\r\n\r\n"
|
|
reader = MockStreamReader(request_data)
|
|
unreader = AsyncUnreader(reader)
|
|
cfg = MockConfig()
|
|
|
|
with pytest.raises(InvalidHTTPVersion):
|
|
await AsyncRequest.parse(cfg, unreader, ("127.0.0.1", 8000))
|