You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
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 #616shape 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:
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:
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".
Filed as an open question, not an asserted defect.
FrameType.post_processseeds its flag accumulator with a bare0and promotes it only as a side effect of|=, so an HTTP/2 frame with no flag bits set leaves__flags__a plainintwhere both the schema and the data model declare it aFlags. 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 currentorigin/main(da381f259).Correction to where this lives
It is not in
pcapkit/protocols/application/httpv2.py— that file has nopost_process. It is in the schema:pcapkit/protocols/schema/application/httpv2.py:129-147One accumulator, on
FrameType(the base schema), inherited by all 11 frame schemas.The declared type says
Flags, and the sibling make path already agreespcapkit/protocols/schema/application/httpv2.py:122—__flags__: 'Flags'(underif TYPE_CHECKING:)pcapkit/protocols/data/application/httpv2.py:39—__value__: 'FrameType.Flags'FrameType.Flagsat:126, per-frame subclasses at:165,:192,:276,:295,:328,:374. Nothing lives underpcapkit/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:Six correct
Flags(0)seeds on the make side against one bare0on the parse side, which is what makesflags = 0look 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 attests/protocols/application/test_http_unit.py:561-570):Promotion analysis.
flags |= valis0 | Flags.BIT_n;Flagsoverrides__ror__viaenum.Flag, so the reflected operation runs and returns aFlags. The0is never replaced wholesale — promotion is purely an operator side effect. When the loop body never runs,self.__flags__ = 0stores a plainint.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 plainint0. Harmless, because all five passflags=Noneinto their data objects (httpv2.py:420, 568, 611, 816, 859) and never surface it.The symptom against a consumer is #616's message verbatim:
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
0x00is routine — every connection's opening SETTINGS frame, every non-final DATA frame, every non-ACK PING. So__value__is a plainintfor 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, athttpv2.py:461,:515,:656,:718,:771,:895, each as'__value__': schema.__flags__. A repo-wide sweep for membership tests, iteration,.nameaccess andFlags-class references found no consumer that would break:httpv2.py:290(int(flags) & (1 << bit)) >> bitint()httpv2.py:315flags_int = int(flags)int()httpv2.py:319, 322, 323, 335flags_int & int(Schema_*Frame.Flags.PADDED)httpv2.py:396data.flags.__value__ if data.flags is not None else 0httpv2.py:461, 515, 656, 718, 771, 895'__value__': schema.__flags__httpv2.py:244flags: 'Flags' = 0, # type: ignore[assignment]makedefault is already a bare0, with a silencing commentNor is it visible in a dump — dictdumper output is identical on both paths (
"__value__": 0/|-- __value__ -> 0). The only observable difference isrepr(). AndFlags = Schema_FrameType.Flagsathttpv2.py:83is insideif TYPE_CHECKING:, soHTTP.FlagsraisesAttributeErrorat runtime — the enum is not public runtime API.The existing test does not pin the type either:
tests/protocols/application/test_http_unit.py:1080isself.assertEqual(plain.__flags__, 0), which passes for both plainint0 andFlags(0), sinceIntFlagcompares equal toint.These are stdlib
enum, notaenum— and it does not change the answerpcapkit/protocols/schema/application/httpv2.py:6isimport enumand:126isclass Flags(enum.IntFlag):, whereas #616's TCP flags areaenum.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: theTypeErrorcomes from the left operand being a plainint(which has no__contains__), which is library-independent;|=promotes identically in both; and stdlibenum.IntFlagacceptsFlags(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 asFlags; 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,
.nameaccess,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)atpcapkit/protocols/schema/application/httpv2.py:139— plus a test assertingisinstance(plain.__flags__, DataFrame.Flags)to replace the type-blindassertEqual(..., 0)attest_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
TCP.read's flag accumulator withEnum_Flags(0), not a no-op cast (#616) #634 (for TCP.read seeds _flags with a no-op cast, so a flagless segment leaves it a plain int #616) and never investigated; this issue is that investigation._make_http_datanever readsframe.flags, soEND_STREAMis silently dropped on a parse → reconstruct round trip. That is _make_http_data never reads frame.flags, so END_STREAM is silently dropped on a parse-reconstruct round trip #652, and it is independent of this one: fixing either leaves the other.TCP.readseedingcast('Enum_Flags', 0), a runtime no-op) and fix(tcp): resolve the connection flags before building the options (#587) #597 (which fixed the same pattern on TCP's make path). The difference from both is that TCP.read seeds _flags with a no-op cast, so a flagless segment leaves it a plain int #616's_flagsis read by the MPTCP dispatchers withEnum_Flags.SYN in self._flags, which is what gave it teeth. There is no equivalent read here.