Skip to content

inject() raises a bare ValueError for a malformed parent_id — D-51 requires INVALID_PARENT_ID, and the driver asserts a tautology #32

Description

@tercel

inject raises a bare ValueError for a malformed parent_id — no error code

src/apcore/trace_context.py:113:

raise ValueError(f"parent_id must be 16 lowercase hex chars, got {parent_id!r}")

Executed live: type(e).__mro__ is (ValueError, Exception, BaseException, object), getattr(e, "code", ...) is absent, and isinstance(e, ModuleError) is False. INVALID_PARENT_ID appears nowhere under src/.

Decision D-51 requires the code INVALID_PARENT_ID. apcore-typescript sets it (src/trace-context.ts:71, (err as Error & {code?: string}).code = 'INVALID_PARENT_ID') and apcore-rust now returns ErrorCode::InvalidParentId (src/errors.rs:183, raised by inject_checked) — that was aiperceivable/apcore-rust#31.

apcore-python is the remaining outlier. A polyglot caller matching on INVALID_PARENT_ID gets it from two SDKs and nothing from the third.

The driver cannot catch this — it asserts the fixture against a literal

tests/conformance/test_trace_context.py:95:

assert expected["error"]["code"] == "INVALID_PARENT_ID"

That compares the fixture to a string constant. It is a tautology: it cannot fail on SDK behaviour, and it is the only place the driver mentions the code — everything else it asserts is the message text. That is why a missing code never surfaced.

This is the third instance of the same anti-pattern found in the 0.26 sweep, after pipeline_failfast_config.json (asserted the class name ConfigurationError, which all three SDKs share, while they emitted three different wire codes) and pipeline_step_middleware.json (asserted wrapped_in against a literal in this same driver style). See aiperceivable/apcore#81.

Fix both together: raise a typed error carrying code="INVALID_PARENT_ID", and rewrite the driver to assert the raised exception's .code rather than the fixture's own value. The rewrite is what keeps the fix from regressing.

Deciding the exception type

ValueError is the Pythonic choice for a bad argument, and callers may already be catching it. Raising a ModuleError subclass that also inherits ValueError preserves both — worth considering rather than a straight swap, since inject is a public API.

Metadata

Metadata

Assignees

No one assigned

    Labels

    bugSomething isn't working

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions