Skip to content

fix(extensions): start fresh on a non-UTF-8 extension registry - #3998

Open
Noor-ul-ain001 wants to merge 1 commit into
github:mainfrom
Noor-ul-ain001:fix/extension-registry-non-utf8
Open

fix(extensions): start fresh on a non-UTF-8 extension registry#3998
Noor-ul-ain001 wants to merge 1 commit into
github:mainfrom
Noor-ul-ain001:fix/extension-registry-non-utf8

Conversation

@Noor-ul-ain001

Copy link
Copy Markdown
Contributor

Problem

ExtensionRegistry._load() treats a corrupted registry as "start fresh", but only for two exception types:

except (json.JSONDecodeError, FileNotFoundError):
    # Corrupted or missing registry, start fresh

A .specify/extensions/.registry file containing invalid UTF-8 bytes raises UnicodeDecodeError from the text-mode read before JSON parsing begins, so it escapes that clause. Because the registry is loaded in __init__, the bare traceback breaks every extension command on the project:

$ specify extension list
...
  File ".../specify_cli/extensions/__init__.py", line 655, in _load
    data = json.load(f)
UnicodeDecodeError: 'utf-8' codec can't decode byte 0xff in position 0: invalid start byte

ExtensionRegistry is also constructed from events.py and from two places in presets/__init__.py, so the crash reaches beyond the extension command group.

Fix

Catch UnicodeDecodeError in the same clause — undecodable bytes are the same corruption class as unparseable JSON, only the exception type differs.

OSError stays uncaught on purpose: the data may be intact on disk, and starting fresh would let a later _save() wipe it (same fail-closed reasoning as the workflow catalog cache loader).

After the fix the command degrades gracefully:

$ specify extension list
No extensions installed.

Relationship to #3955

This is the exact twin of the PresetRegistry._load() fix merged in #3955. The two registries are parallel implementations of the same pattern and only the preset side was corrected there; this applies the identical one-line change plus rationale comment to the extension side.

Notably, _get_installed_sibling_ids() already documented and worked around this precise gap at its own call site:

UnicodeError is caught alongside OSError because ExtensionRegistry._load() opens the file in text mode and only handles JSONDecodeError / FileNotFoundError, so a registry file with non-UTF-8 bytes would otherwise surface a UnicodeDecodeError here and break every config read instead of degrading gracefully.

That comment is updated to reflect that _load() now handles the case itself; the local catch is kept as belt-and-braces against regression.

Testing

Added test_load_starts_fresh_for_non_utf8_registry, mirroring the #3955 preset test: writes undecodable bytes to .registry, then asserts the registry starts fresh and list() / is_installed() behave.

Verified the test genuinely covers the fix — with src/specify_cli/extensions/__init__.py reverted it fails with the original UnicodeDecodeError, and passes with the fix applied.

pytest tests/test_extensions.py → 489 passed. pytest tests/test_presets.py tests/integrations/test_events.py → 678 passed. The only failures in either run are pre-existing symlink tests that require Windows elevation (WinError 1314) and fail identically on a clean checkout. ruff check passes on both changed files.

🤖 Generated with Claude Code

ExtensionRegistry._load() catches json.JSONDecodeError and
FileNotFoundError to start fresh on a corrupted or missing registry, but
a .registry file with invalid UTF-8 bytes raised UnicodeDecodeError from
the text-mode read before JSON parsing began. Because the registry is
loaded in __init__, that bare traceback broke every extension command --
`specify extension list` on such a project exits with a raw
UnicodeDecodeError instead of the module's clean path.

Catch UnicodeDecodeError in the same clause: undecodable bytes are the
same corruption class as unparseable JSON, only the exception type
differs. OSError stays uncaught on purpose -- the data may be intact on
disk, and starting fresh would let a later _save() wipe it.

This is the exact twin of the PresetRegistry._load() fix in github#3955; the
two registries are parallel implementations and only the preset side was
corrected. _get_installed_sibling_ids() already worked around this gap
locally by catching UnicodeError at its own call site; its comment is
updated to reflect that _load() now handles the case itself, with the
local catch kept as belt-and-braces against regression.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
@Noor-ul-ain001
Noor-ul-ain001 requested a review from mnriem as a code owner August 5, 2026 16:36
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant