Skip to content

fix(integrations): validate cached catalog shape before returning it#3627

Merged
mnriem merged 3 commits into
github:mainfrom
jawwad-ali:fix/integration-catalog-cache-shape
Jul 22, 2026
Merged

fix(integrations): validate cached catalog shape before returning it#3627
mnriem merged 3 commits into
github:mainfrom
jawwad-ali:fix/integration-catalog-cache-shape

Conversation

@jawwad-ali

Copy link
Copy Markdown
Contributor

What

IntegrationCatalog's fetch validates a fresh payload's shape (root must be a JSON object, integrations must be a mapping) before use — but the cache-read branch returned the cached object directly:

if age < self.CACHE_DURATION:
    return json.loads(cache_file.read_text(encoding="utf-8"))   # no shape check

So a poisoned or older-format cache (e.g. {"integrations": []}) is returned as-is and later crashes with AttributeError: 'list' object has no attribute 'items' when the caller iterates integrations.

Fix

Validate the cached object with the same key checks as the fresh-fetch path before returning it. The raised ValueError is already caught by the existing surrounding except (… ValueError …), which deletes the corrupt cache files and refetches from source — the same recovery a poisoned network payload already gets.

Test

test_poisoned_cache_shape_is_dropped_and_refetched: a fresh-but-mis-shaped cache (integrations as a list) is dropped and the valid source refetched — fails before (AttributeError), passes after.


🤖 Written with the assistance of Claude Code (AI). Bug self-found; fix/tests verified locally (fail-before / pass-after), ruff clean.

The catalog cache-read branch returned json.loads(cache_file) directly, skipping
the shape validation the fresh-fetch branch enforces (dict root + 'integrations'
mapping). A poisoned or older-format cache (e.g. {"integrations": []}) was
therefore returned as-is and later crashed with 'AttributeError: list object has
no attribute items' when the caller iterated integrations. Validate the cached
object the same way; the raised ValueError is already caught by the surrounding
handler, which drops the corrupt cache and refetches from source.

Test: a fresh-but-mis-shaped cache is dropped and the valid source refetched
(fails before: AttributeError).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Validates cached integration catalogs before reuse to prevent malformed data from causing downstream crashes.

Changes:

  • Adds cache payload shape validation and recovery.
  • Adds a regression test for malformed cached integrations.
  • One issue remains: cached payloads can omit schema_version.
Show a summary per file
File Description
src/specify_cli/integrations/catalog.py Validates cached catalog structure before returning it.
tests/integrations/test_integration_catalog.py Tests malformed-cache deletion and refetching.

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: 1
  • Review effort level: Medium

Comment thread src/specify_cli/integrations/catalog.py Outdated
@mnriem

mnriem commented Jul 22, 2026

Copy link
Copy Markdown
Collaborator

Please address Copilot feedback

jawwad-ali and others added 2 commits July 22, 2026 18:43
… fetch

Address review: the cache-read path checked only that the payload was a
dict with a dict 'integrations', while the fresh-fetch path also required
'schema_version'. That asymmetry let an older/poisoned cache such as
{"integrations": {}} (no schema_version) bypass the format contract
instead of being dropped and refetched.

Introduce a shared `_catalog_shape_error()` helper and use it in both
paths so they enforce the same contract (dict + schema_version + dict
integrations). The fresh path still raises IntegrationCatalogError with
the "Invalid catalog format from <url>" prefix; the cache path still
raises ValueError (caught to drop+refetch). Add a test for the
missing-schema_version cache case.

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…ctly

Replace the integration-level missing-schema_version cache test (which
was masked by multi-source merging — a sibling catalog source still
supplied the entry, so it passed regardless of the fix) with a direct
unit test of _catalog_shape_error. This deterministically proves both
paths now reject a payload missing schema_version, a non-dict
integrations, or a non-dict payload, and accept a well-formed one.

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
@mnriem
mnriem requested a review from Copilot July 22, 2026 13:50
@jawwad-ali

Copy link
Copy Markdown
Contributor Author

Fixed in 0f362d3: extracted a shared _catalog_shape_error() validator and use it in both the fresh-fetch and cache-read paths, so they now enforce the same contract (dict + schema_version + dict integrations). A cache like {"integrations": {}} (missing schema_version) is now dropped and refetched instead of bypassing validation. Replaced the integration-level test (it was masked by multi-source merging) with a direct unit test of the validator.

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Review details

  • Files reviewed: 2/2 changed files
  • Comments generated: 0 new
  • Review effort level: Medium

@mnriem
mnriem merged commit a6743ab into github:main Jul 22, 2026
14 checks passed
@mnriem

mnriem commented Jul 22, 2026

Copy link
Copy Markdown
Collaborator

Thank you!

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.

3 participants