fix(integrations): wrap a non-UTF-8 catalog response - #4011
Merged
mnriem merged 3 commits intoAug 7, 2026
Conversation
`_fetch_single_catalog` decodes the response body with `.decode("utf-8")`
before handing it to `json.loads`. A non-UTF-8 body therefore raises
`UnicodeDecodeError`, which is a sibling of `json.JSONDecodeError` under
`ValueError` rather than a subclass of it, so neither the `URLError` nor the
`JSONDecodeError` handler catches it.
The raw exception escapes `_get_merged_integrations`, whose
`except IntegrationCatalogError` is specifically designed to warn and skip a
bad catalog and carry on with the remaining ones. One catalog served over a
misconfigured proxy or truncated mid-multibyte-sequence thus takes down
`specify integration search` entirely instead of degrading to a warning.
Wrap it in `IntegrationCatalogError`, matching the convention already used
for the same decode in `authentication/azure_devops.py`, which lists
`UnicodeDecodeError` alongside `JSONDecodeError`.
Note that the cache-read path in this same method already tolerates this via
its `UnicodeError` clause; only the network path was unguarded.
Two regression tests: one pins the wrapped-error contract on the fetch, and
one covers the behaviour that actually motivates it — a broken catalog is
skipped with a warning while a healthy sibling catalog still resolves.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
The raw-bytes helper patched `open_url` wholesale, which skipped the real URL validation and redirect handling inside it. This module already imports `route_opener_open_through_urlopen`, the repo's shared fixture that routes `build_opener().open()` back through `urlopen` for exactly this reason, so patching `urlopen` instead keeps the stub effective while still exercising `open_url` itself. Renamed to `_patch_urlopen_bytes` to sit alongside the existing `_patch_urlopen`, whose signature it now mirrors. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
The previous commit reverted the source change by accident while reworking the tests, leaving the regression tests passing against an unfixed module. Restores the `except UnicodeDecodeError` clause. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Contributor
There was a problem hiding this comment.
Pull request overview
Wraps non-UTF-8 integration catalog responses in IntegrationCatalogError, preserving multi-catalog search resilience.
Changes:
- Converts
UnicodeDecodeErrorinto a catalog-specific error. - Adds regression coverage for direct fetching and skipping malformed catalogs.
Show a summary per file
| File | Description |
|---|---|
src/specify_cli/integrations/catalog.py |
Handles non-UTF-8 network responses. |
tests/integrations/test_integration_catalog.py |
Tests error wrapping and fallback behavior. |
Review details
Tip
Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
- Files reviewed: 2/2 changed files
- Comments generated: 0
- Review effort level: Balanced
Collaborator
|
Thank you! |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Problem
IntegrationCatalog._fetch_single_catalogdecodes the response body before parsing it:A non-UTF-8 body raises
UnicodeDecodeErrorat.decode(), beforejson.loadsever runs.UnicodeDecodeErrorandjson.JSONDecodeErrorare siblings underValueError, not parent/child:So neither the
except urllib.error.URLErrornor theexcept json.JSONDecodeErrorhandler catches it, and the raw exception escapes the method.Why it matters
The caller,
_get_merged_integrations, is explicitly written to tolerate one bad catalog:Because
UnicodeDecodeErroris not anIntegrationCatalogError, that resilience is bypassed. A single catalog served through a misconfigured proxy, or truncated mid-multibyte-sequence, abortsspecify integration searchwith a bare traceback instead of degrading to a warning — even when every other configured catalog is healthy.Worth noting the cache-read path in this same method already handles this, via
UnicodeErrorin itsexcepttuple. Only the network path was unguarded, so the failure is inconsistent between a cache hit and a cache miss.Fix
Wrap it in the module's own error type. This matches the convention already established in
authentication/azure_devops.py, which namesUnicodeDecodeErroralongsideJSONDecodeErroraround the identicalread_response_limited(...).decode("utf-8")call.Scope checked, deliberately not changed
I swept every
read_response_limited(...).decode("utf-8")call site:workflows/catalog.py(both the workflow and step catalog fetches) andbundler/services/adapters.py— already covered by a broadexcept Exception.authentication/azure_devops.py— already namesUnicodeDecodeError._version.py::_fetch_latest_release_tag— intentionally left alone. Its docstring states: "On anything else — including a malformed response body — the exception propagates; there is no catch-all (research D-006)." That is a deliberate design decision, not a gap.integrations/catalog.pywas the one genuine outlier.Tests
Two regression tests in
TestCatalogFetch:test_fetch_wraps_non_utf8_catalog_response— pins the wrapped-error contract.test_search_skips_non_utf8_catalog— covers the behaviour that actually motivates the fix: a broken catalog is skipped with a warning on stderr while a healthy sibling catalog still resolves.Both fail on
mainwith the rawUnicodeDecodeError: 'utf-8' codec can't decode byte 0xff in position 35: invalid start byte, and pass with the fix. Full file suite: 123 passed.ruff checkclean.The new tests need raw bytes on the wire, which the existing
_patch_urlopenhelper cannot express (it JSON-encodes its input), so they use a small sibling helper that patchesopen_urlto serve raw bodies keyed by URL.🤖 Generated with Claude Code