Skip to content

fix(const,vendor): make Method.get case-sensitive, per RFC 9110 (#896) - #907

Merged
JarryShaw merged 1 commit into
mainfrom
fix/896-method-get-case-sensitive
Sep 29, 2026
Merged

JarryShaw merged 1 commit into
mainfrom
fix/896-method-get-case-sensitive

Conversation

@JarryShaw

Copy link
Copy Markdown
Owner

Please follow the guide below

What is the purpose of your pull request?

  • fix — corrects a defect

Description of your pull request and other information

Closes #896. Method.get upper-cased its key before matching, so Method.get('get') resolved to Method.GET -- RFC 9110 §9.1 makes the HTTP method token case-sensitive, so get is a distinct, non-standardised method.

Fixed in pcapkit/vendor/http/method.py's own get() template (this crawler renders its own, not the shared default one) and regenerated pcapkit/const/http/method.py -- verified byte-reproducible offline from a CSV fixture reconstructed from the committed file, no live IANA crawl.

Command.get (FTP) is untouched: RFC 959 §4.1 makes FTP command codes case-insensitive, so its equivalent override stays as-is.

Did not delete the override in favour of the base EnumRegistry.get, as the issue's suggested mechanism proposed: measured directly, the base raises KeyError for an unmatched string key with no default rather than building an unregistered member, so deleting it would have made Method.get('get') raise instead of resolving to a pseudo-member preserving 'get''s casing -- the behaviour the issue and this PR's own tests require.

Updated three pre-existing tests that pinned the old case-insensitive behaviour (#582/#583) and docs/source/conventions.rst's illustration of it, and added a dedicated test module for the direct repro, the unregistered-member casing guarantee, and crawler/generated-file parity.

Method.get upper-cased its key before matching, so Method.get('get')
resolved to the standardised Method.GET member -- RFC 9110 Section 9.1
makes the HTTP method token case-sensitive, so a lower-case get is a
distinct, non-standardised method, not an alias of GET.

- pcapkit/vendor/http/method.py: the get() override in this crawler's
  own LINE template now matches against `key` itself rather than
  `key.upper()`, and falls through to build an unregistered member
  (preserving the caller's own casing) exactly as before on a miss.
  Command.get (FTP) is untouched: RFC 959 Section 4.1 makes FTP
  command codes case-insensitive, so its equivalent override stays.
- pcapkit/const/http/method.py regenerated from that template; the
  diff is confined to get()'s body and docstring. Verified
  byte-reproducible across two independent offline regenerations fed
  from a CSV fixture reconstructed from the committed file (no live
  IANA crawl).
- Updated three pre-existing tests that pinned the old
  case-insensitive Method.get behaviour (#582/#583), and
  docs/source/conventions.rst's illustration of the same. Added
  tests/const/test_const_method_case_sensitive_896_unit.py for the
  direct repro, the unregistered-member casing guarantee, and the
  crawler/generated-file parity check.
- Did not delete the override in favour of EnumRegistry.get, as the
  issue suggested: measured directly, the base's get() raises
  KeyError for an unmatched str key with no default, rather than
  building an unregistered member -- deleting it would have made
  Method.get('get') raise instead of resolving to a pseudo-member
  preserving 'get', which is what issue #896 and the tests both
  require.

Build/tests: `coverage run -m pytest tests/const/ tests/protocols/application/test_http_unit.py`
green (264 tests / 40325 subtests in tests/const/, 60 tests / 387
subtests in test_http_unit.py); pylint and mypy report no new
findings against pcapkit/const/http/method.py and
pcapkit/vendor/http/method.py.
@JarryShaw JarryShaw added fix Pull requests that fix a defect (fix: subject prefix) const Regenerated IANA or vendor constant tables; members keep their numeric values breaking Breaks public-facing behaviour or API (apply alongside the type label) review: pending No verdict for the current head - never reviewed, or the head moved since the last one labels Sep 29, 2026
@JarryShaw

Copy link
Copy Markdown
Owner Author

GOOD TO GO on 99e430cc6 — cross-review (opus; author sonnet). Clean on every claim, and it found two pre-existing defects plus a better implementation shape, none of which block.

The base-get question is settled, and you were right to refuse my instruction. Measured independently on FEATCode, which runs the base unmodified:

base get(miss, no default)           -> RAISED KeyError
base get(miss, default=registered)   -> the REGISTERED member (not a new one)
base get(miss, default=unregistered) -> RAISED KeyError
FEATCode('zz-nope')  [constructor]   -> unregistered member, registered: False

The base never builds an unregistered member for a str miss on any branch. Only the constructor does. So deleting the override, as this issue's original text told you to, would have made Method.get('get') raise. Keeping it and removing only the fold is correct.

Verified: case sensitivity holds with caller casing preserved on the value and the name canonicalised, no minting, membership snapshot-checked at 40 unchanged. Command (FTP) unregressed — and the reviewer checked pcapkit/vendor/ftp/command.py too, which is the file that would reintroduce a fold on the next crawl; also untouched. Byte-reproducible from the template, render-vs-committed matching at e425d943… — which is the load-bearing comparison, not render-vs-render. Census does not move, confirmed by running the file: 74 tests OK. All three re-pinned test files strengthened rather than weakened — the renamed test went from one assertIs loop to six assertions per key, and no new entry was added to any exception set across the whole 638-line diff. Each new test fails pre-fix, three independent spot-checks, including one that pins the crawler half separately.

A live defect it uncovered, filed as #908. Method.get does no value-match, so the two IANA methods whose name ≠ value mis-resolve. Verified myself:

BASELINE_CONTROL -> value 'BASELINE-CONTROL', registry idempotent=True
Method.get('BASELINE-CONTROL')  is BASELINE_CONTROL? False   idempotent=False   <- wrong
Method('BASELINE-CONTROL')      is BASELINE_CONTROL? True

Same for VERSION-CONTROL (both RFC 3253). And _RE_METHOD = rb"(?P<method>[A-Z][A-Z-]*)\Z" at httpv1.py:60 admits hyphens, so a real capture reaches :434 and pcapkit reports the wrong idempotent. Pre-existing and unchanged by this PR — .upper() is a no-op on an already-uppercase token — so not a regression, but this PR touches the exact line that would fix it.

The reviewer's third option would fix it for free, and is worth noting for #908: try: return super().get(key) / except KeyError: return Method._unregistered_member(...) keeps case-sensitivity and the unregistered-member fallback and picks up the base's _value2member_map_ value-match that the hand-rolled override lacks.

One inconsistency inside a file this PR edits, not blocking because I scoped it out myself. _missing_ still folds case and its docstring still reads "Matched case-insensitively against the canonical upper-case member names" with no RFC caveat — fifteen lines from a get whose new docstring says RFC 9110 requires the opposite. A reader of that one file gets two rationales and no pointer between them. My correction on #896 scoped this issue to get alone, so it is a deliberate ruling; recording it on #908 rather than reopening here.

A merge-order hazard I own, and git will not flag it. This PR adds three :meth:~pcapkit.corekit.enum.EnumRegistry.get`` cross-references — tests/const/test_const_enum_no_mint.py:2267, `tests/const/test_const_method_case_sensitive_896_unit.py:25` and `:29`. #906 renames that to `EnumLookup.get`. The two PRs' `conventions.rst` hunks are ten lines apart and auto-merge cleanly, so nothing conflicts — but whoever merges second leaves three stale references behind. I will fix them in whichever order they land.

Ready for you to merge. I am not merging.

@JarryShaw JarryShaw added review: good-to-go Cross-review at the current head says ready; CI state is separate and removed review: pending No verdict for the current head - never reviewed, or the head moved since the last one labels Sep 29, 2026
@JarryShaw
JarryShaw merged commit ef4ba1a into main Sep 29, 2026
63 checks passed
@JarryShaw
JarryShaw deleted the fix/896-method-get-case-sensitive branch September 29, 2026 05:03
@JarryShaw JarryShaw removed the review: good-to-go Cross-review at the current head says ready; CI state is separate label Sep 29, 2026
JarryShaw added a commit that referenced this pull request Sep 29, 2026
…ONTROL resolve (#908)

`Method.get` checked only `_member_map_` (names), never `_value2member_map_`
(values), so `BASELINE_CONTROL`/`'BASELINE-CONTROL'` and
`VERSION_CONTROL`/`'VERSION-CONTROL'` -- whose name and value differ, a
hyphen being unusable in an identifier -- fell through to an unregistered
member with the wrong `safe`/`idempotent`. `httpv1.py`'s permissive method
regex lets a real capture reach this.

- Delegate to the base `EnumLookup.get`, keeping the override only for the
  unregistered fallback; moved `staticmethod` -> `classmethod` since
  zero-argument `super()` needs `cls` to bind.
- Kept `default`'s existing meaning (the fallback member's value) rather
  than the base's `NO_DEFAULT`/registered-value-only semantics, which would
  be caller-visible.
- Case-sensitivity (#896/#907) and caller's-own-casing (#860) are
  unchanged, each pinned by its own test; fixed in the crawler template and
  the generated file together, verified byte-identical.

Added an end-to-end `httpv1` test and a template/generated-file parity test.

Build: `coverage run -m unittest` over `tests/const/` and
`test_http_unit.py`, all passing.

Document and pin the non-`str` surface, which this fix moves as a side
effect. Delegating to the base routes a non-`str` key through `cls(key)` and
so `_missing_`, which raises `ValueError`; `except KeyError` catches only a
failed *name* lookup, so that `ValueError` reaches the caller. Measured
before and after:

    get(42)      AttributeError: 'int' object has no attribute 'upper'
                 -> ValueError: 42 is not a valid Method
    get(None)    AttributeError  -> ValueError
    get(b'GET')  RETURNED name=b'GET' value=b'GET'
                 -> ValueError: b'GET' is not a valid Method

The bytes row is a return-to-raise change, and bytes is the plausible
mistake: `_RE_METHOD` is a bytes pattern and `httpv1` is bytes throughout,
so `httpv1.py:434` is safe only because it wraps the match in
`self.decode(...)`. Raising is the intended behaviour -- a bytes-valued
`Method` is not something this registry should hand back -- so the fix is a
`Raises:` clause plus four pins in a new `NonStrKeyTests`, all four of which
fail against unmodified main. Added to the crawler template as well as the
generated module, so regeneration cannot drop it.
JarryShaw added a commit that referenced this pull request Sep 29, 2026
…ONTROL resolve (#908)

`Method.get` checked only `_member_map_` (names), never `_value2member_map_`
(values), so `BASELINE_CONTROL`/`'BASELINE-CONTROL'` and
`VERSION_CONTROL`/`'VERSION-CONTROL'` -- whose name and value differ, a
hyphen being unusable in an identifier -- fell through to an unregistered
member with the wrong `safe`/`idempotent`. `httpv1.py`'s permissive method
regex lets a real capture reach this.

- Delegate to the base `EnumLookup.get`, keeping the override only for the
  unregistered fallback; moved `staticmethod` -> `classmethod` since
  zero-argument `super()` needs `cls` to bind.
- Kept `default`'s existing meaning (the fallback member's value) rather
  than the base's `NO_DEFAULT`/registered-value-only semantics, which would
  be caller-visible.
- Case-sensitivity (#896/#907) and caller's-own-casing (#860) are
  unchanged, each pinned by its own test; fixed in the crawler template and
  the generated file together, verified byte-identical.

Added an end-to-end `httpv1` test and a template/generated-file parity test.

Build: `coverage run -m unittest` over `tests/const/` and
`test_http_unit.py`, all passing.

Document and pin the non-`str` surface, which this fix moves as a side
effect. Delegating to the base routes a non-`str` key through `cls(key)` and
so `_missing_`, which raises `ValueError`; `except KeyError` catches only a
failed *name* lookup, so that `ValueError` reaches the caller. Measured
before and after:

    get(42)      AttributeError: 'int' object has no attribute 'upper'
                 -> ValueError: 42 is not a valid Method
    get(None)    AttributeError  -> ValueError
    get(b'GET')  RETURNED name=b'GET' value=b'GET'
                 -> ValueError: b'GET' is not a valid Method

The bytes row is a return-to-raise change, and bytes is the plausible
mistake: `_RE_METHOD` is a bytes pattern and `httpv1` is bytes throughout,
so `httpv1.py:434` is safe only because it wraps the match in
`self.decode(...)`. Raising is the intended behaviour -- a bytes-valued
`Method` is not something this registry should hand back -- so the fix is a
`Raises:` clause plus four pins in a new `NonStrKeyTests`, all four of which
fail against unmodified main (failures=2, errors=2; four distinct method
names). The docstring pin reads the source via `inspect.getsource(Method.get)`
rather than `Method.get.__func__`, which a `staticmethod` does not carry --
against the pre-fix module the `__func__` form died with `AttributeError`
before any assertion ran, so it pinned `classmethod`-ness rather than the
clause it is named for. Added to the crawler template as well as the
generated module, so regeneration cannot drop it.
JarryShaw added a commit that referenced this pull request Sep 29, 2026
…ONTROL resolve (#908)

`Method.get` checked only `_member_map_` (names), never `_value2member_map_`
(values), so `BASELINE_CONTROL`/`'BASELINE-CONTROL'` and
`VERSION_CONTROL`/`'VERSION-CONTROL'` -- whose name and value differ, a
hyphen being unusable in an identifier -- fell through to an unregistered
member with the wrong `safe`/`idempotent`. `httpv1.py`'s permissive method
regex lets a real capture reach this.

- Delegate to the base `EnumLookup.get`, keeping the override only for the
  unregistered fallback; moved `staticmethod` -> `classmethod` since
  zero-argument `super()` needs `cls` to bind.
- Kept `default`'s existing meaning (the fallback member's value) rather
  than the base's `NO_DEFAULT`/registered-value-only semantics, which would
  be caller-visible.
- Case-sensitivity (#896/#907) and caller's-own-casing (#860) are
  unchanged, each pinned by its own test; fixed in the crawler template and
  the generated file together, verified byte-identical.

Added an end-to-end `httpv1` test and a template/generated-file parity test.

Build: `coverage run -m unittest` over `tests/const/` and
`test_http_unit.py`, all passing.

Document and pin the non-`str` surface, which this fix moves as a side
effect. Delegating to the base routes a non-`str` key through `cls(key)` and
so `_missing_`, which raises `ValueError`; `except KeyError` catches only a
failed *name* lookup, so that `ValueError` reaches the caller. Measured
before and after:

    get(42)      AttributeError: 'int' object has no attribute 'upper'
                 -> ValueError: 42 is not a valid Method
    get(None)    AttributeError  -> ValueError
    get(b'GET')  RETURNED name=b'GET' value=b'GET'
                 -> ValueError: b'GET' is not a valid Method

The bytes row is a return-to-raise change, and bytes is the plausible
mistake: `_RE_METHOD` is a bytes pattern and `httpv1` is bytes throughout,
so `httpv1.py:434` is safe only because it wraps the match in
`self.decode(...)`. Raising is the intended behaviour -- a bytes-valued
`Method` is not something this registry should hand back -- so the fix is a
`Raises:` clause plus four pins in a new `NonStrKeyTests`, all four of which
fail against unmodified main (failures=2, errors=2; four distinct method
names). The docstring pin reads the source via `inspect.getsource(Method.get)`
rather than `Method.get.__func__`, which a `staticmethod` does not carry --
against the pre-fix module the `__func__` form died with `AttributeError`
before any assertion ran, so it pinned `classmethod`-ness rather than the
clause it is named for. Added to the crawler template as well as the
generated module, so regeneration cannot drop it.
@JarryShaw JarryShaw added this to the 1.5 milestone Oct 6, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

breaking Breaks public-facing behaviour or API (apply alongside the type label) const Regenerated IANA or vendor constant tables; members keep their numeric values fix Pull requests that fix a defect (fix: subject prefix)

Projects

Status: Done

Development

Successfully merging this pull request may close these issues.

fix(const): Method.get is case-insensitive, but RFC 9110 makes the method token case-sensitive

1 participant