firegex/tests/unit/test_llhttp_binding.py
2026-09-13 15:54:56 +02:00

97 lines
3.6 KiB
Python

"""The HTTP parser binding stands on its own.
`firegex._llhttp` is a fork of `pyllhttp`, and for a while it was still *calling* it: the
error path imported the module `pyllhttp` to fetch the exception class to raise. That
package is archived, is not a dependency, and is installed on no machine the wheel is
installed on — so the import failed, its traceback went to stderr (which on a filter is
the service log), and the function returned without setting an exception. CPython reports
that to the caller as `SystemError: ... returned NULL without setting an exception`, and
a malformed request — the traffic this parser exists to look at — said exactly that
instead of naming the problem.
It went unnoticed because a machine that once installed `pyllhttp` has it, and both the
laptop and the VM these tests were written on did. So the cases below take it away first.
"""
import sys
import pytest
class _Blocked:
"""A meta-path finder that makes one module unimportable, as if it were not there."""
def __init__(self, name: str):
self.name = name
def find_spec(self, name, path=None, target=None):
if name == self.name:
raise ImportError(f"No module named {self.name}")
return None
@pytest.fixture
def without_pyllhttp():
"""The state every machine that installs the wheel is in."""
blocker = _Blocked("pyllhttp")
sys.meta_path.insert(0, blocker)
saved = sys.modules.pop("pyllhttp", None)
try:
yield
finally:
sys.meta_path.remove(blocker)
if saved is not None:
sys.modules["pyllhttp"] = saved
def test_the_parser_names_itself():
"""The types and the exceptions belong to this module, under this module's name."""
from firegex import _llhttp
assert _llhttp.Request.__module__ == "firegex._llhttp"
assert _llhttp.Response.__module__ == "firegex._llhttp"
assert _llhttp.Error.__module__ == "firegex._llhttp"
def test_a_parse_error_raises_this_modules_exception(without_pyllhttp):
"""…and raises it with nothing else installed to borrow it from."""
from firegex import _llhttp
parser = _llhttp.Request()
with pytest.raises(_llhttp.InvalidMethodError) as raised:
parser.execute(b"NOTAMETHOD / HTTP/1.1\r\n\r\n")
assert isinstance(raised.value, _llhttp.Error)
assert type(raised.value).__module__ == "firegex._llhttp"
# The message is llhttp's own reason, not the shape of the failure to find a class.
assert "method" in str(raised.value).lower()
def test_every_error_code_has_a_class_to_raise(without_pyllhttp):
"""A code with no class would be the silent half of the old bug coming back.
`RuntimeError` is the floor here, deliberately: whatever goes wrong looking the class
up, something is raised, because returning without an exception set is what produced
a `SystemError` naming a package the operator has never heard of.
"""
from firegex import _llhttp
# One representative per kind of thing that can go wrong mid-message.
cases = [
(b"NOTAMETHOD / HTTP/1.1\r\n\r\n", "request"),
(b"GET / HTTP/1.1\r\nHost: a\r\nTransfer-Encoding: \r\n\r\n", "request"),
(b"HTTP/1.1 200 OK\nContent-Length: 0\r\n\r\n", "response"),
]
for payload, kind in cases:
parser = _llhttp.Request() if kind == "request" else _llhttp.Response()
with pytest.raises(_llhttp.Error):
parser.execute(payload)
def test_a_valid_exchange_is_unaffected(without_pyllhttp):
from firegex import _llhttp
response = _llhttp.Response()
response.execute(b"HTTP/1.1 404 Not Found\r\nContent-Length: 0\r\n\r\n")
assert response.status_code == 404