Skip to content

MH MN-ID: bytes and str are documented interchangeably but each subtype accepts only one, and the wrong one fails with a bare stdlib exception #469

Description

@JarryShaw

Summary

MH._make_opt_mn_id documents bytes | str as accepted identifier types for
every MN-ID subtype, but each subtype's field accepts only one of the two.
Passing the other builds a schema that cannot be packed, and fails with a bare
stdlib exception rather than an in-library one.

This is the same family as #467 (which covered int) and #448 (which covered
the IPv6_Address width), but it is about bytes/str and is not fixed by
either.

Reproduction

Measured on fa128959e (current main). Identical on PR #468's branch, so
pre-existing and not a regression from that PR — I checked both trees.

from pcapkit.const.mh.option import Option
from pcapkit.const.mh.mn_id_subtype import MNIDSubtype
from pcapkit.protocols.internet.mh import MH

proto = object.__new__(MH)

# NAI's field is text, so bytes cannot be encoded
proto._make_opt_mn_id(Option.MN_ID_OPTION_TYPE,
                      subtype=MNIDSubtype.NAI,
                      identifier=b'node@example').pack()

# IMSI's field is raw octets, so str cannot be packed
proto._make_opt_mn_id(Option.MN_ID_OPTION_TYPE,
                      subtype=MNIDSubtype.IMSI,
                      identifier='12345').pack()
NAI  + bytes -> AttributeError: 'bytes' object has no attribute 'encode'
IMSI + str   -> struct.error: argument for 's' must be a bytes object
NAI  + str   -> length=13, packs 15 octets            (correct)

So the defect is symmetric: bytes fails for the one text subtype, and str
fails for the octet subtypes. Only the matching pairing works.

Both failures happen at pack() rather than at construction, and neither is an
instance of pcapkit.utilities.exceptions.BaseError`, so a caller cannot catch
them with the library's own exception hierarchy.

Why it matters

The type union is documented, not accidental — _make_opt_mn_id's identifier
parameter is annotated bytes | str | IPv6Address | int, and the
Schema_MNIDOption.__init__ stub carries a matching union. So the signature
promises bytes and str interchangeably for all subtypes when in fact the
choice is fixed per subtype by whether that subtype's field is a StringField
or a BytesField.

NAI is the only text subtype; IMSI, P_TMSI, EUI_48_address,
EUI_64_address, GUTI and DUID are octet subtypes.

Suggested direction

The same shape as #467's fix: decide per subtype, then either convert (encode a
str for the octet subtypes, decode bytes for NAI — noting that an encoding
has to be chosen and named if so) or reject with a ProtocolError saying which
type that subtype takes. #467's fix already added exactly that kind of
per-subtype rejection for int, so the mechanism and message style are in
place to extend.

Whichever is chosen, the documented union should end up describing what is
actually accepted.

Provenance

Surfaced while verifying #468 (the fix for #467). Its author flagged the
bytes-for-NAI half and correctly left it out of scope; the str-for-octet-
subtypes half is mine, found while measuring the first. Both verified on main
and on the PR branch before filing.

Activity

  1. JarryShaw commented on Sep 18, 2026

    @JarryShaw
    OwnerAuthor

    Folding in a third member of this family, found by the re-review of #468 and
    verified here on both trees.

    _make_opt_mn_id also lets a bare TypeError escape from
    pcapkit/protocols/internet/mh.py:7717 — id_len = len(identifier) — for an
    identifier that supports neither int nor __len__:

    identifier=None                              main: TypeError   #468 branch: TypeError
    identifier=1.5                               main: TypeError   #468 branch: TypeError
    identifier=IPv6Address('::1'), subtype=NAI   main: NO RAISE, length=17    #468 branch: TypeError
    

    The first two are pre-existing and unchanged. The third is a behaviour change
    worth noting separately:
    on main, handing an IPv6Address to the text NAI
    subtype raises nothing at all and declares length=17, sizing it as though it
    were an IPv6 address — silently wrong. #468 turns that into a loud
    TypeError, which is an improvement in kind even though the exception type is
    still not in-library.

    So the full shape of this issue is that _make_opt_mn_id accepts four documented
    identifier types while each subtype's field accepts exactly one of them, and
    every mismatch escapes as a bare stdlib exception rather than a
    ProtocolError:

    • bytes for the text subtype (NAI) — AttributeError: 'bytes' object has no attribute 'encode'
    • str for the six octet subtypes — struct.error: argument for 's' must be a bytes object
    • anything without __len__ or int behaviour — TypeError: object of type 'X' has no len()

    int is already handled: #468 rejects it with ProtocolError for every subtype
    but IPv6_Address. Extending that same per-subtype rejection to cover the cases
    above would close this issue in one piece, and would also remove the
    silently-wrong IPv6Address-for-NAI path that main still has.

  2. added
    bugIssues reporting a defect (set by the bug report template; a default, not an assessment)
    on Sep 22, 2026
  3. added this to the 1.5 milestone on Oct 6, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    bugIssues reporting a defect (set by the bug report template; a default, not an assessment)

    Projects

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions