protocols: bind HTTP.make to a real instance, and wrap SETTINGS' item schema - #462
Conversation
… schema - HTTP.make dispatched with `protocol.make(**kwargs)`, calling the versioned `make` unbound on the imported class. Neither HTTPv1.make nor HTTPv2.make is a staticmethod/classmethod, so every real call raised `TypeError: make() missing 1 required positional argument: 'self'`. Existing tests missed this because their fakes declare `make` as `@staticmethod`, which absorbs an unbound call. Fixed by calling `make` on a bare instance built via `protocol.__new__(protocol)`, bypassing `__init__`'s parse/pack machinery since there is no file/length to construct from when building from kwargs. Closes #452. - SettingsFrame.settings passed the bare SettingPair class as ListField's item_type instead of a field instance, silenced by a `# type: ignore [arg-type]` that mypy had right. Wrapped it as every sibling ListField does: `item_type=SchemaField(schema=SettingPair)`, and dropped the suppression. Closes #459. Regression tests exercise the real HTTPv1/HTTPv2 classes and the real ListField wiring rather than fakes, since fakes are what hid both defects. mypy: 124 errors in 40 files at da24227, unchanged by either fix. Full suite passes with fixtures generated once beforehand.
|
After-suite result, on this branch's tip 997 collected (994 established baseline at Run with Against the established |
|
Standing in for Copilot (out of tokens) on this review. Reviewed at head CIAll 23 checks settled green: 21 SUCCESS, 2 SKIPPED ( Being 3 behind
|
The docstring said the SETTINGS pack path dies on KeyError: 'length'. It dies on KeyError: 'flags', raised by FrameType.post_process at schema/application/httpv2.py:144 reaching the enclosing header's flags field through a nested packet context that cannot see it. 'length' was carried over from #445's CGA Parameters symptom, which is a genuinely different field in a different module, and the wrong wording travelled from that issue into #459's filing and then into here. The EXPECTED_FAILURES entry for httpv2-frame/SETTINGS had the right symptom throughout and is now cited. Comment text only -- no code, no assertions, no behaviour change.
PR #462 merged (bind HTTP.make to a real instance, and wrap SETTINGS' item schema, closing #459) since this branch's last merge, and pulled in via the fast-forward to 0e7abbe. SettingsFrame.settings now wraps its item_type in SchemaField(schema=SettingPair) instead of passing the raw class, so the AttributeError this entry recorded no longer happens. Verified directly: tests/protocols/test_option_roundtrip_unit.py's httpv2-frame/SETTINGS case now returns 'OK'. This is what failed CI on Python 3.12 and Integration Python 3.15 at 0e7abbe -- not an interpreter-dependent or order-dependent failure, the same stale-entry mismatch on every interpreter; those two jobs simply reported first. Verified: mypy pcapkit -> 123 errors/40 files (unchanged). Round-trip harness, this file's own tests, and tests/protocols/internet/ test_mh_unit.py: 46 passed, 629 subtests passed, 0 failed.
main gained #461 (closing #458: prepare's @prepare decorator now distinguishes a declared zero length from a derived one, raising StreamEOFError only for the latter) since this branch's previous merge, on top of #462 (closing #459, already handled). Between the two, all three remaining httpv2-frame entries this PR's own fix had exposed -- DATA, HEADERS, CONTINUATION -- now round-trip cleanly too. Verified directly against the round-trip harness: all three return 'OK'. Rewrote the HTTP/2 section's comment block to summarise all six frames' history (PUSH_PROMISE/PING via #445 itself, DATA/HEADERS/ CONTINUATION via #461, SETTINGS via #462) now that none of them need an entry. Also merged origin/main (fa12895, #461) -- clean auto-merge, no conflicts, confirmed against #464's changes to tests/protocols/internet/test_mh_unit.py (different hunks, and its own test run clean: 62 passed, 272 subtests, 0 failed). Verified: mypy pcapkit -> 124 errors/40 files (main at fa12895: 125, one more than its previous 124 -- unrelated to this branch, and this branch stays one fewer than whatever main's own count is, from the same pre-existing type: ignore cleanup as before). Round-trip harness: 7 passed, 363 subtests passed, 0 failed. mh-extension/* re-verified 'OK' again on this merge (all four).
Summary
Two single-site application-layer fixes, batched because both are HTTP-family corrections in files nobody else owns, not because they are related.
#452 —
HTTP.makecalls the versionedmakeunbound on the classpcapkit/protocols/application/http.py:134(now:147) was:protocolis the imported class (httpv1.HTTPorhttpv2.HTTP), andmakeis an ordinary instance method on both —httpv1.py:147isdef make(self, ...), matching the abstractProtocolBase.make(self, **kwargs)atprotocol.py:258.inspect.getattr_static(HTTPv1, 'make')returns a plainfunction, confirming it is neitherstaticmethodnorclassmethod. So the call leftselfunfilled and raisedTypeErrorfor every real call.Corrected reproduction (the issue's original one called
HTTP.make(version=1, ...)directly on the class, which fails at the outerHTTP.make— itself an ordinary instance method — and proves nothing about the inner dispatch; see the correction posted on the issue). The real, supported entry point is construction, sinceProtocolBase.__init__'s**kwargs-only overload (protocol.py:517) callsself.pack(**kwargs), andpack(protocol.py:284) isself.__header__ = self.make(**kwargs)— a bound call on the outerHTTPinstance that reaches the inner, previously-unboundprotocol.make(**kwargs):The second is an in-library error raised inside
HTTPv1.makefrom deliberately minimal keyword arguments — proof the dispatch now reaches the callee and the callee is validating, not merely the absence of aTypeError.Fix and why:
protocol.__new__(protocol).make(**kwargs)— construct a bare instance of the versioned class (bypassing__init__'s parse/pack machinery via__new__) and call the now-boundmakeon it. This is a third option beyond the two the issue named (construct throughread'sprotocol(self._data, length, **kwargs)path, or make the versionedmakeaclassmethod/staticmethod):protocol(**kwargs)construction doesn't fit: with nofile,__post_init__would callself.pack(**kwargs)(which callsmakeanyway) and then alsoself.unpack(...)→self.read(...), parsing the bytes right back — a full pack-then-reparse round trip when only the pack half was asked for, and a different, heavier operation than "construct and call make."makeaclassmethod/staticmethodchanges a signature shared with the abstractProtocolBase.make(self, **kwargs), andHTTPv2.makegenuinely needs instance access (self.__frame__,self._lookup_registry,getattr(self, meth_name, ...)), so it isn't a drop-in classmethod without deeper rework.protocol.__new__(protocol)works today because neitherHTTPv1.makenorHTTPv2.makereads any state that__init__/__post_init__establishes (HTTPv1.makeonly callsself._make_index, already aclassmethod;HTTPv2.makeonly readsself.__frame__, a class-level registry). What would break it: a futuremakeoverride on either class that reads instance state set up during__init__(e.g.self._data,self._info) would need a different dispatch here.I checked whether this file's existing unit tests use this same
object.__new__idiom as precedent and want to be precise rather than overclaim:test_http_read_make_and_guess_version_delegation_pathsuses@staticmethodfakes, which is exactly what hid this bug and is not precedent. Other tests in the same file (test_httpv1_id_make_data_and_request_construction,test_httpv2_id_length_make_bytes_and_register_frame_warning) do callobject.__new__(HTTPv1)/object.__new__(HTTPv2)on the real classes before calling.make(...)directly, so the specific__new__-bypass technique does have real-class precedent in this file, even though no existing test exercises it throughHTTP.make's own dispatch.#459 —
SettingsFrame.settingspasses a Schema class where a field belongspcapkit/protocols/schema/application/httpv2.py:SettingPairis aSchemasubclass (aSchemaMeta), not a field instance. Confirmed the defect directly: with the bare class,ListField.unpack's schema branch doesfield = self._item_type(packet)→ constructs aSettingPairfrom the packet dict instead of configuring a per-item field, and theisinstance(self._item_type, SchemaField)check (which selects the schema branch) isFalsefor a bare class in the first place — so it falls into the plain-field branch and fails there:AttributeError: 'SettingPair' object has no attribute 'length'.Fix:
item_type=SchemaField(schema=SettingPair), matching every siblingListField(tcp.py'sSACK.sack,hip.py,mh.py'sCGAParametersOption.parameters,sctp.py'sgap_blocks/dup_tsn), and the# type: ignore[arg-type]is removed. mypy confirms the removal is clean: 124 errors in 40 files atda2422728, identical before and after this change (baseline measured fresh at the actual base commit — the issue's cited "128 errors in 41 files" was measured 4 commits earlier ate2d8ed6d1and has since drifted for unrelated reasons). Neither this file norhttp.pyappears anywhere in the mypy output, before or after.What stays unobservable until PR #457 lands: #459 is masked by #445 — the SETTINGS frame's pack path dies earlier with
KeyError: 'flags'— raised byFrameType.post_processatschema/application/httpv2.py:144, where it reaches the enclosing header'sflagsfield through a nested packet context that cannot see it — before ever reaching thesettingsfield. Confirmed viatests/protocols/test_option_roundtrip_unit.py: thehttpv2-frame/SETTINGSEXPECTED_FAILURESentry still fails the exact same recorded way (CONSTRUCT: KeyError: 'flags') with this fix applied — noEXPECTED_FAILURESentries changed or need to change. So the regression test here is a unit-level assertion directly against the field wiring (SettingsFrame.__fields__['settings']) and a realSettingPairunpack over hand-built bytes, not an end-to-endHTTPv2round trip — that remains blocked on #457.Regression tests
Both new tests deliberately exercise real classes rather than fakes, since fakes with a compatible
staticmethod/__call__are what hid each defect:test_http_make_dispatches_to_real_versioned_classesandtest_http_construction_reaches_the_versioned_make_callee(the corrected, construction-based reproduction) intests/protocols/application/test_http_unit.py, against the realHTTPv1/HTTPv2classes.test_settings_frame_settings_field_wraps_item_schema, against the realSettingsFrame.__fields__['settings']field andSettingPairschema.Before/after, quoted:
Test plan
--collect-only: 994 collected at baseda2422728(corroborated independently by PR schema: a forward match consumes nothing, so bill it nothing in len(schema) #456's and PR protocols: floor CALIPSO, MPL and REG_INFO's wire-derived lengths at zero #460's agents against the same base).examples/generators/make_samples.pybeforehand — result posted as a PR comment once the run lands.tests/protocols/test_option_roundtrip_unit.py: all 7 tests / 299 subtests pass;httpv2-frame/SETTINGS's recorded failure is unchanged. NoEXPECTED_FAILURESchanges.da2422728, unchanged by either fix; the removed# type: ignore[arg-type]produces no new "unused ignore" or other error.Branched from
da2422728(baseorigin/mainhas since advanced to94a93e721, a docs-onlypep.rstdelivery-sequence addition that does not touch code, so the base was not moved to chase it).Closes #452
Closes #459