Skip to content

bug: infrahubctl schema load - display_schema_load_errors crashes with ValueError when error path traverses an extensions: block #1007

@iddocohen

Description

@iddocohen

Component

Python SDK, infrahubctl

Infrahub SDK version

1.20.0

Current Behavior

infrahubctl schema load calls infrahub_sdk.ctl.schema.display_schema_load_errors to render server-rejected schema validation errors. The function indexes loc_path[2] and loc_path[4] and casts both to int, assuming the loc tuple shape is ("body", "schemas", <schema_index>, "nodes"|"generics", <node_index>, ...).

When the rejected error originates inside an extensions: block (per docs.infrahub.app/topics/schema-extensions), the loc tuple has an extra segment:

("body", "schemas", <schema_index>, "extensions", "nodes"|"generics"|"relationships", <node_index>, <field>)

So loc_path[4] is a string like "generics" instead of the integer node index. int("generics") raises:

ValueError: invalid literal for int() with base 10: 'generics'

The traceback replaces the intended human-readable error message — the user never sees what the server actually rejected.

Expected Behavior

The server's rejection should be rendered in the same human-readable format as non-extensions errors, e.g.:

Unable to load the schema:
  Node: DcimGenericDevice (extensions/generics) | Extra inputs are not permitted (extra_forbidden) — field 'include_in_menu' is not part of the extension schema

Steps to Reproduce

Environment

  • Infrahub server: 1.9.2 (any 1.5+ exhibits the rejection that triggers the formatter)
  • infrahub-sdk[all] 1.20.0
  • Python 3.12

Steps

1. Have a running Infrahub instance with a schema that includes a generic that has include_in_menu: true (e.g., the DcimGenericDevice from opsmill/schema-library's base/dcim.yml). Any kind whose existing schema metadata you want to override works.

2. Author a schema-extension YAML that attempts to override a metadata field on an existing kind:

# repro_extension.yml
---
version: "1.0"
extensions:
  generics:
    - kind: DcimGenericDevice
      include_in_menu: false

3. Run infrahubctl schema load:

infrahubctl schema load repro_extension.yml

Actual output (1.20.0)

Schema loading.....
Unable to load the schema:
Traceback (most recent call last):
  File ".../site-packages/infrahub_sdk/ctl/schema.py", line 87, in load
    display_schema_load_errors(response=response.errors, schemas_data=schemas_data)
  File ".../site-packages/infrahub_sdk/ctl/schema.py", line 68, in display_schema_load_errors
    node_index = int(loc_path[4])
                 ^^^^^^^^^^^^^^^^
ValueError: invalid literal for int() with base 10: 'generics'

The user sees no human-readable explanation of why the schema was rejected.

Additional Information

Affected code

infrahub_sdk/ctl/schema.py lines 53–103 (in 1.20.0), function display_schema_load_errors:

def display_schema_load_errors(response: dict[str, Any], schemas_data: list[SchemaFile]) -> None:
    console.print("[red]Unable to load the schema:")
    if "detail" not in response:
        handle_non_detail_errors(response=response)
        return

    for error in response["detail"]:
        loc_path = error.get("loc", [])
        if not valid_error_path(loc_path=loc_path):
            continue

        # if the len of the path is equal to 6, the error is at the root of the object
        # if the len of the path is higher than 6, the error is in an attribute or a relationships
        schema_index = int(loc_path[2])
        node_index = int(loc_path[4])           # <-- crashes here for extensions paths
        ...

And the path validator at line 116, which lets the malformed path through:

def valid_error_path(loc_path: list[Any]) -> bool:
    return len(loc_path) >= 6 and loc_path[0] == "body" and loc_path[1] == "schemas"

valid_error_path does not check that loc_path[3] is "nodes" or "generics", so loc_path[3] == "extensions" slips through and the indexing assumption breaks.

Suggested fix

Branch on whether loc_path[3] == "extensions" and re-anchor the indexing:

def display_schema_load_errors(response: dict[str, Any], schemas_data: list[SchemaFile]) -> None:
    console.print("[red]Unable to load the schema:")
    if "detail" not in response:
        handle_non_detail_errors(response=response)
        return

    for error in response["detail"]:
        loc_path = error.get("loc", [])
        if not valid_error_path(loc_path=loc_path):
            continue

        schema_index = int(loc_path[2])

        # Distinguish top-level vs extensions-block error paths.
        # Top-level:  body/schemas/<si>/nodes|generics/<ni>/<field>
        # Extensions: body/schemas/<si>/extensions/nodes|generics|relationships/<ni>/<field>
        if loc_path[3] == "extensions":
            container = loc_path[4]                 # 'nodes' / 'generics' / 'relationships'
            node_index = int(loc_path[5])
            tail = loc_path[6:]
        else:
            container = loc_path[3]                 # 'nodes' / 'generics'
            node_index = int(loc_path[4])
            tail = loc_path[5:]
        ...

get_node and the downstream len(loc_path) == 6 / > 6 branches need corresponding adjustments (the absolute lengths shift by one for the extensions path).

valid_error_path should additionally accept loc_path[3] in ("nodes", "generics", "extensions") so unrelated server errors don't fall through this codepath.

Metadata

Metadata

Assignees

No one assigned

    Labels

    type/bugSomething isn't working as expected

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions