Skip to content

open question: FrameType.post_process seeds __flags__ with a bare 0, so a flagless HTTP/2 frame leaves it a plain int #650

Description

@JarryShaw

Filed as an open question, not an asserted defect. FrameType.post_process seeds its flag accumulator with a bare 0 and promotes it only as a side effect of |=, so an HTTP/2 frame with no flag bits set leaves __flags__ a plain int where both the schema and the data model declare it a Flags. That is the #616 shape exactly, and it reproduces end to end. What it is missing is #616's impact: no consumer anywhere in the library, its tests, its docs or its dump output observes the difference. Whether that makes it a defect or pre-emptive hardening is the open question.

Measured on 375e9d411, CPython 3.14.7, tree asserted against the worktree rather than the editable install. Both files below are byte-identical on current origin/main (da381f259).

Correction to where this lives

It is not in pcapkit/protocols/application/httpv2.py — that file has no post_process. It is in the schema:

pcapkit/protocols/schema/application/httpv2.py:129-147

    def post_process(self, packet: 'dict[str, Any]') -> 'Schema':
        ...
        flags = 0                                                    # 139
        for key, val in filter(lambda kv: kv[0].startswith('BIT_'),
                               self.Flags.__members__.items()):
            name = key.lower()
            if packet['flags'][name]:
                flags |= val                                         # 144

        self.__flags__ = flags                                       # 146
        return self

One accumulator, on FrameType (the base schema), inherited by all 11 frame schemas.

The declared type says Flags, and the sibling make path already agrees

  • pcapkit/protocols/schema/application/httpv2.py:122 — __flags__: 'Flags' (under if TYPE_CHECKING:)
  • pcapkit/protocols/data/application/httpv2.py:39 — __value__: 'FrameType.Flags'
  • the enum is nested in the schema classes: FrameType.Flags at :126, per-frame subclasses at :165, :192, :276, :295, :328, :374. Nothing lives under pcapkit/const/http/ — that package has no flags module.

The decisive asymmetry: the construct side in the same package already seeds it correctly, six times — pcapkit/protocols/application/httpv2.py:

 949        flags = Schema_DataFrame.Flags(0)
 999        flags = Schema_HeadersFrame.Flags(0)
1097        flags = Schema_SettingsFrame.Flags(0)
1155        flags = Schema_PushPromiseFrame.Flags(0)
1189        flags = Schema_PingFrame.Flags(0)
1281        flags = Schema_ContinuationFrame.Flags(0)

Six correct Flags(0) seeds on the make side against one bare 0 on the parse side, which is what makes flags = 0 look like an oversight rather than a decision.

Measured — reproduces through the public parse path

Parsing real frame bytes via HTTP(io.BytesIO(raw), len(raw)).read() (note this library's length field counts the whole frame, header included — see the comment at tests/protocols/application/test_http_unit.py:561-570):

DATA flags=0x00 (none set)     __value__ type=int    repr=0
DATA flags=0x01 (END_STREAM)   __value__ type=Flags  repr=<Flags.END_STREAM: 1>
DATA flags=0x08 (PADDED)       __value__ type=Flags  repr=<Flags.PADDED: 8>

Promotion analysis. flags |= val is 0 | Flags.BIT_n; Flags overrides __ror__ via enum.Flag, so the reflected operation runs and returns a Flags. The 0 is never replaced wholesale — promotion is purely an operator side effect. When the loop body never runs, self.__flags__ = 0 stores a plain int.

A third path: five frame schemas define no BIT_ members at all (UnassignedFrame, PriorityFrame, RSTStreamFrame, GoawayFrame, WindowUpdateFrame), so for those the filter is empty and __flags__ is unconditionally plain int 0. Harmless, because all five pass flags=None into their data objects (httpv2.py:420, 568, 611, 816, 859) and never surface it.

The symptom against a consumer is #616's message verbatim:

END_STREAM in __value__  ->  TypeError: argument of type 'int' is not a container or iterable
control with Flags(0):  END_STREAM in DataFrame.Flags(0) -> False
and DataFrame.Flags(0) == 0 -> True   (so seeding Flags(0) is backward compatible)

Worth noting: the broken path is the common case, not the edge case. Unlike #616, where an all-zero TCP flags octet is unusual, an HTTP/2 flags byte of 0x00 is routine — every connection's opening SETTINGS frame, every non-final DATA frame, every non-ACK PING. So __value__ is a plain int for most frames in most captures.

Why it is an open question: every consumer is int-safe

Six frame types surface __flags__ into the data object, at httpv2.py:461, :515, :656, :718, :771, :895, each as '__value__': schema.__flags__. A repo-wide sweep for membership tests, iteration, .name access and Flags-class references found no consumer that would break:

Site Use Safe on a plain int?
httpv2.py:290 (int(flags) & (1 << bit)) >> bit yes, explicit int()
httpv2.py:315 flags_int = int(flags) yes, explicit int()
httpv2.py:319, 322, 323, 335 flags_int & int(Schema_*Frame.Flags.PADDED) yes, int bitwise
httpv2.py:396 data.flags.__value__ if data.flags is not None else 0 yes, pass-through
httpv2.py:461, 515, 656, 718, 771, 895 '__value__': schema.__flags__ yes, just stores it
httpv2.py:244 flags: 'Flags' = 0, # type: ignore[assignment] the public make default is already a bare 0, with a silencing comment

Nor is it visible in a dump — dictdumper output is identical on both paths ("__value__": 0 / |-- __value__ -> 0). The only observable difference is repr(). And Flags = Schema_FrameType.Flags at httpv2.py:83 is inside if TYPE_CHECKING:, so HTTP.Flags raises AttributeError at runtime — the enum is not public runtime API.

The existing test does not pin the type either: tests/protocols/application/test_http_unit.py:1080 is self.assertEqual(plain.__flags__, 0), which passes for both plain int 0 and Flags(0), since IntFlag compares equal to int.

These are stdlib enum, not aenum — and it does not change the answer

FrameType.Flags.__mro__ = (<flag 'Flags'>, <flag 'IntFlag'>, <class 'int'>, <enum 'ReprEnum'>, <flag 'Flag'>, <enum 'Enum'>, <class 'object'>)
module of the base enum  = enum
aenum.IntFlag in mro?    = False
TCP Flags.__mro__       = (<aenum 'Flags'>, <aenum 'IntFlag'>, ...)

pcapkit/protocols/schema/application/httpv2.py:6 is import enum and :126 is class Flags(enum.IntFlag):, whereas #616's TCP flags are aenum.IntFlag (pcapkit/const/tcp/flags.py:15). Recording this explicitly because mis-stating the enum library is how #623's root cause went wrong once already. It does not change the diagnosis: the TypeError comes from the left operand being a plain int (which has no __contains__), which is library-independent; |= promotes identically in both; and stdlib enum.IntFlag accepts Flags(0) for an empty flag set, so the one-token fix is available either way.

The boundary of what was established

Established: the seed is a plain int; it promotes only when a bit is set; the no-bits path is reachable through the public parse path and is the common case; both the schema and the data model declare the type as Flags; the make path already uses the correct idiom six times; the fix is backward-compatible for every existing consumer.

Not established: any consumer — in pcapkit, its tests, its docs, or its dump output — that observes the difference. None was found, after searching for membership tests, iteration, .name access, Flags-class references and serialization differences.

The decision this needs: whether a declared-type violation on a reachable path counts as a defect here in its own right. If yes, the fix is one token — flags = self.Flags(0) at pcapkit/protocols/schema/application/httpv2.py:139 — plus a test asserting isinstance(plain.__flags__, DataFrame.Flags) to replace the type-blind assertEqual(..., 0) at test_http_unit.py:1080. If no, this is a latent-hazard note and the honest framing is "pre-emptive hardening against the #616 class", not "bug fix".

Notes

Activity

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions