Summary
MultiDict.get() / OrderedMultiDict.get() emit a logging record at ERROR level every time a key is absent, then correctly return the default. On a successful parse of a capture containing unfragmented IPv6 with reassembly enabled, this puts one ERROR line on stderr per IPv6 frame, for what the calling code itself describes as the ordinary case.
file:line
pcapkit/utilities/exceptions.py:112-113, in BaseError.__init__ (lines 100-117):
100 def __init__(self, *args: 'Any', quiet: 'bool' = False, **kwargs: 'Any') -> 'None':
101 # log error
102 if not quiet:
103 if DEVMODE:
104 logger.critical('%s: %s', type(self).__name__, str(self),
105 exc_info=self if VERBOSE else False,
106 stack_info=VERBOSE, stacklevel=-stacklevel())
107 else:
108 logger.critical("%s: %s", type(self).__name__, str(self))
109
110 # logger.error('%s: %s', type(self).__name__, str(self), exc_info=self,
111 # stack_info=True, stacklevel=-stacklevel())
112 else:
113 logger.error('%s: %s', type(self).__name__, str(self))
The offending expression is line 113:
logger.error('%s: %s', type(self).__name__, str(self))
quiet=True selects that branch. So quiet currently means "log at ERROR instead of CRITICAL", not "do not log".
Intent vs behaviour
docs/source/pcapkit/utilities/exceptions.rst:14 documents the parameter as:
:param quiet: If :data:`True`, suppress exception message.
How an ordinary lookup reaches it
pcapkit/corekit/multidict.py:210-214 — get is defined on MultiDict and inherited by OrderedMultiDict:
210 def get(self, key: '_KT', default: '_VT | _T' = None) -> '_VT | _T':
211 try:
212 return self[key]
213 except MissingKeyError:
214 return default
Both __getitem__ implementations raise with quiet=True:
183 raise MissingKeyError(key, quiet=True) # MultiDict.__getitem__ (169-183)
514 raise MissingKeyError(key, quiet=True) # OrderedMultiDict.__getitem__ (511-514)
The IPv6 caller is pcapkit/toolkit/pcap.py:101-104 (identical code in pcapkit/toolkit/pcapng.py:104-107), whose own comment says a miss is expected:
101 if (ipv6_frag := ipv6.extension_headers.get( # type: ignore[call-overload]
102 Enum_ExtensionHeader.IPv6_Frag
103 )) is None: # dismiss not fragmented frame
104 return None
Observable symptom
pcapkit/utilities/logging.py:49-55 attaches a StreamHandler(sys.stderr) at level INFO unconditionally, so these records reach the terminal with no logging configuration at all.
On examples/captures/ipv6.pcap (16 frames, parse succeeds):
[INFO] 09/14/2026 12:24:22 AM - IPv6 reassembly enabled
[ERROR] 09/14/2026 12:24:22 AM - MissingKeyError: <ExtensionHeader.IPv6_Frag: 44>
[ERROR] 09/14/2026 12:24:22 AM - MissingKeyError: <ExtensionHeader.IPv6_Frag: 44>
[ERROR] 09/14/2026 12:24:22 AM - MissingKeyError: <ExtensionHeader.IPv6_Frag: 44>
... (12 in total)
[WARNING] 09/14/2026 12:24:22 AM - EOF reached
frames parsed: 16
reassembled ipv6 datagrams: 1
Counted programmatically (Python 3.14.7):
| capture |
frames |
records by level |
MissingKeyError ERROR records |
examples/captures/ipv6.pcap |
16 |
{'INFO': 1, 'ERROR': 12, 'WARNING': 1} |
12 |
examples/captures/http6.cap |
26 |
{'INFO': 1, 'ERROR': 26, 'WARNING': 3} |
26 |
Every one of them is the identical string MissingKeyError: <ExtensionHeader.IPv6_Frag: 44>.
Scope, measured on ipv6.pcap — IPv6 reassembly has to be on:
config frames MissingKeyError ERRORs
default (no reassembly) 16 0
reassembly=True only 16 0
reassembly=True, ip=True 16 12
reassembly=True, ipv6=True 16 12
reassembly=True, ipv4=True 16 0
This corrects the way it was first described to me as happening on "any capture containing unfragmented IPv6" — reassembly must be enabled, and the CLI exposes no reassembly flag, so it is a library-API path. The minimal form below has no preconditions at all, though.
Minimal reproduction
No capture and no reassembly needed:
import logging
from pcapkit.corekit.multidict import OrderedMultiDict
class Sink(logging.Handler):
def __init__(self):
super().__init__()
self.records = []
def emit(self, record):
self.records.append(record)
sink = Sink()
logging.getLogger('pcapkit').addHandler(sink)
result = OrderedMultiDict().get('absent')
print('returned:', repr(result))
for r in sink.records:
print(r.levelname, '|', r.getMessage())
Output:
returned: None
ERROR | MissingKeyError: 'absent'
The capture-level form:
import pcapkit
pcapkit.extract(fin='examples/captures/ipv6.pcap', nofile=True, store=True,
reassembly=True, ipv6=True)
Related side effect in the same constructor
Lines 115-116 run on both branches, so the same absent-key .get() also sets sys.tracebacklimit = 0 for the rest of the process, truncating tracebacks for entirely unrelated exceptions:
sys.tracebacklimit before: <unset>
traceback lines for an unrelated ValueError, before: 14
OrderedMultiDict().get("absent") returned: None
sys.tracebacklimit after : 0
traceback lines for the same unrelated ValueError, after: 1
What a fix would need to touch
Two defensible places; the second is arguably the root cause.
pcapkit/utilities/exceptions.py, BaseError.__init__, the else branch at 112-113 — make quiet=True mean what the docs say (emit nothing, or emit at DEBUG). Also decide whether sys.tracebacklimit = 0 at 115-116 should apply on the quiet path.
pcapkit/corekit/multidict.py — MultiDict.get (210-214) with MultiDict.__getitem__ (169-183) and OrderedMultiDict.__getitem__ (511-514): a .get() miss is control flow, not an error, and need not construct a logging exception at all. Fixing here removes the per-lookup cost as well as the noise and leaves quiet semantics alone.
Other callers passing quiet=True, which a semantics change would affect: pcapkit/corekit/multidict.py:183, pcapkit/corekit/multidict.py:514, pcapkit/protocols/protocol.py:835.
Tests pinning current behaviour: tests/utilities/test_exceptions_warnings.py:22-25 and :27-38.
Observed vs inferred
- Observed by running code: the single ERROR record from a bare
.get() miss on both MultiDict and OrderedMultiDict; the correct None return; 12 and 26 ERROR records on ipv6.pcap and http6.cap; that they appear on stderr with no logging configuration; the trigger matrix; the sys.tracebacklimit = 0 side effect and its effect on an unrelated traceback.
- Inferred by reading code: that
docs/source/pcapkit/utilities/exceptions.rst:14 is the authoritative statement of what quiet is meant to do; that pcapkit/toolkit/pcapng.py:104-107 behaves the same as the pcap.py path (identical code, not separately exercised).
Related
Two other defects in the same family, filed separately because no single change fixes more than one of them: the double/triple emission in pcapkit/utilities/warnings.py:52-54, and the process-global warnings.filters mutation at pcapkit/utilities/warnings.py:74. Note the structural rhyme with the latter — both are constructors with unscoped process-global side effects.
Summary
MultiDict.get()/OrderedMultiDict.get()emit aloggingrecord at ERROR level every time a key is absent, then correctly return the default. On a successful parse of a capture containing unfragmented IPv6 with reassembly enabled, this puts one ERROR line on stderr per IPv6 frame, for what the calling code itself describes as the ordinary case.file:line
pcapkit/utilities/exceptions.py:112-113, inBaseError.__init__(lines 100-117):The offending expression is line 113:
quiet=Trueselects that branch. Soquietcurrently means "log at ERROR instead of CRITICAL", not "do not log".Intent vs behaviour
docs/source/pcapkit/utilities/exceptions.rst:14documents the parameter as:How an ordinary lookup reaches it
pcapkit/corekit/multidict.py:210-214—getis defined onMultiDictand inherited byOrderedMultiDict:Both
__getitem__implementations raise withquiet=True:The IPv6 caller is
pcapkit/toolkit/pcap.py:101-104(identical code inpcapkit/toolkit/pcapng.py:104-107), whose own comment says a miss is expected:Observable symptom
pcapkit/utilities/logging.py:49-55attaches aStreamHandler(sys.stderr)at level INFO unconditionally, so these records reach the terminal with no logging configuration at all.On
examples/captures/ipv6.pcap(16 frames, parse succeeds):Counted programmatically (Python 3.14.7):
MissingKeyErrorERROR recordsexamples/captures/ipv6.pcap{'INFO': 1, 'ERROR': 12, 'WARNING': 1}examples/captures/http6.cap{'INFO': 1, 'ERROR': 26, 'WARNING': 3}Every one of them is the identical string
MissingKeyError: <ExtensionHeader.IPv6_Frag: 44>.Scope, measured on
ipv6.pcap— IPv6 reassembly has to be on:This corrects the way it was first described to me as happening on "any capture containing unfragmented IPv6" — reassembly must be enabled, and the CLI exposes no reassembly flag, so it is a library-API path. The minimal form below has no preconditions at all, though.
Minimal reproduction
No capture and no reassembly needed:
Output:
The capture-level form:
Related side effect in the same constructor
Lines 115-116 run on both branches, so the same absent-key
.get()also setssys.tracebacklimit = 0for the rest of the process, truncating tracebacks for entirely unrelated exceptions:What a fix would need to touch
Two defensible places; the second is arguably the root cause.
pcapkit/utilities/exceptions.py,BaseError.__init__, theelsebranch at 112-113 — makequiet=Truemean what the docs say (emit nothing, or emit atDEBUG). Also decide whethersys.tracebacklimit = 0at 115-116 should apply on the quiet path.pcapkit/corekit/multidict.py—MultiDict.get(210-214) withMultiDict.__getitem__(169-183) andOrderedMultiDict.__getitem__(511-514): a.get()miss is control flow, not an error, and need not construct a logging exception at all. Fixing here removes the per-lookup cost as well as the noise and leavesquietsemantics alone.Other callers passing
quiet=True, which a semantics change would affect:pcapkit/corekit/multidict.py:183,pcapkit/corekit/multidict.py:514,pcapkit/protocols/protocol.py:835.Tests pinning current behaviour:
tests/utilities/test_exceptions_warnings.py:22-25and:27-38.Observed vs inferred
.get()miss on bothMultiDictandOrderedMultiDict; the correctNonereturn; 12 and 26 ERROR records onipv6.pcapandhttp6.cap; that they appear on stderr with no logging configuration; the trigger matrix; thesys.tracebacklimit = 0side effect and its effect on an unrelated traceback.docs/source/pcapkit/utilities/exceptions.rst:14is the authoritative statement of whatquietis meant to do; thatpcapkit/toolkit/pcapng.py:104-107behaves the same as thepcap.pypath (identical code, not separately exercised).Related
Two other defects in the same family, filed separately because no single change fixes more than one of them: the double/triple emission in
pcapkit/utilities/warnings.py:52-54, and the process-globalwarnings.filtersmutation atpcapkit/utilities/warnings.py:74. Note the structural rhyme with the latter — both are constructors with unscoped process-global side effects.