Repository navigation
fix(const): Method.get is case-insensitive, but RFC 9110 makes the method token case-sensitive #896
Description
Describe the bug
Method.get looks up HTTP method names case-insensitively. RFC 9110 makes the method token case-sensitive, so this conflates two distinct wire tokens.
pcapkit/const/http/method.py:232-242:
@staticmethod
def get(key: 'str', default: 'Optional[str]' = None) -> 'Method':
...
name = key.upper()Its docstring justifies this as "Looked up case-insensitively, since member names are canonicalised to upper case on registration" — a statement about this library's own member naming, not about the protocol.
What the specification says
RFC 9110 §9.1:
The method token is case-sensitive because it might be used as a gateway to object-based systems with case-sensitive method names. By convention, standardized methods are defined in all-uppercase US-ASCII letters.
Contrast RFC 9110 §5.1, which is a different thing and genuinely is case-insensitive:
Field names are case-insensitive and ought to be registered within the "Hypertext Transfer Protocol (HTTP) Field Name Registry"
And RFC 959 §4.1, which is why pcapkit.const.ftp.command.Command's equivalent override is correct and should stay:
The command codes are four or fewer alphabetic characters. Upper and lower case alphabetic characters are to be treated identically.
Reproduction
Source-read at origin/main, not run against a capture. Method.get('get') upper-cases to GET and resolves to the standardised member. Per RFC 9110 a request whose method token is get is a distinct, non-standardised method.
Expected behavior
Method.get should be case-sensitive — i.e. drop the override and use the base EnumRegistry.get, so a lowercase token resolves to an unregistered member carrying the caller's own casing rather than to GET.
Note the base already preserves caller casing for an unregistered value, and the existing override deliberately does too (its own comment says the value is "key exactly as given -- never name"), so the change is narrower than it looks: what moves is only whether a lowercase token matches a registered member.
System information
Source-only; no library version involved. Read at origin/main.
Additional context
Surfaced on #877 while settling that issue's case-folding ruling. The maintainer's ruling there is that enums keep the registrar's own spelling and that case-insensitivity applies only where the RFC recognises it — Command (FTP) qualifies under that test, Method (HTTP) does not. This issue is the concrete consequence; #877 remains the design question about where get lives.
Metadata
Metadata
Assignees
Labels
Projects
- StatusShow more project fieldsDone
Activity
JarryShaw commentedon Sep 29, 2026
Correcting this issue's own Expected behavior section — I wrote it and it was wrong.
I said:
The second half does not follow from the first. The base
getraises for an unmatchedstrkey; it never builds an unregistered member for a miss. Measured onFEATCode, a cleanstrregistry with no override:So deleting the override would have turned
Method.get('get')from "resolves toGET" into "raises" — not into "resolves to an unregistered member preserving'get'", which is what the rest of that section asks for. Those are different outcomes and I conflated them.PR #907 took the correct shape instead: keep
Method's override, remove only the.upper()fold from the match, and leave the canonicalized-name / preserved-value fallback intact. That gives case-sensitive matching and the unregistered-member-preserving-caller-casing behaviour the issue actually wants. The implementer found this by measuring rather than following my instruction, which is the right call.Two other things worth recording from the implementation:
The override originates in the crawler, not the generated file.
pcapkit/vendor/http/method.pycarries its ownLINEtemplate rather than using the shared one inpcapkit/vendor/default.py, and that template'sget()held the case-folding logic. So the fix went in the template and the const file was regenerated — which is the house rule, and worth noting because a reader looking only atpcapkit/const/http/method.pywould conclude the override was hand-added there.Real HTTP/1 parsing is unaffected, so this is breaking only at the API contract. The single production call site is
pcapkit/protocols/application/httpv1.py:434, and it is gated by_RE_METHOD = re.compile(rb"(?P<method>[A-Z][A-Z-]*)\Z")— uppercase-only by construction. So no capture changes behaviour; what changes is what a direct external caller gets for a non-canonical casing, which is exactly what three pre-existing tests had pinned as the old contract.The census in
tests/const/test_const_registry_protocol.pydoes not move:Methodstill has a bespokeget(), just with different matching logic, and is stillStrEnum-valued, so it stays inEXCLUDED_FROM_INTENUM_CENSUSfor the same reason as before.fix(const): value-lookup in Method.get, so BASELINE-CONTROL/VERSION-C…
fix(const): value-lookup in Method.get, so BASELINE-CONTROL/VERSION-C…
fix(const): value-lookup in Method.get, so BASELINE-CONTROL/VERSION-C…