Skip to content

fix: handle Unraid API 4.37.8 nullable roots and disabled introspection - #137

Merged
ruaan-deysel merged 4 commits into
mainfrom
fix/135-api-4378-compat
Oct 30, 2026
Merged

ruaan-deysel merged 4 commits into
mainfrom
fix/135-api-4378-compat

Conversation

@ruaan-deysel

@ruaan-deysel ruaan-deysel commented Oct 30, 2026 •

Copy link
Copy Markdown
Collaborator

Pull Request

Description

Fixes #135. These are the two breaking changes from an audit of upstream Unraid API releases 4.37.7–4.37.9 (the library is aligned through 4.37.6, #129).

  1. Nullable docker/vms roots (unraid/api#2123, 4.37.8). A failed Docker or VM service now returns {"docker": null} plus errors instead of data: null. query() returned that as a partial result, and every reader's result.get("docker", {}).get(...) raised AttributeError. ha-unraid only catches the library's exceptions for these optional services, so one stopped service would fail its whole system poll. Now query() raises UnraidAPIError (with the GraphQL errors) when errors are present and every returned root is null, and the nine readers read the roots null-safely. A failed service is never reported as an empty list, which would make consumers prune entities.
  2. Introspection disabled by default (unraid/api#2095 in 4.37.8 and #2129 in 4.37.9). These servers answer HTTP 400 with extensions.code: INTROSPECTION_DISABLED. get_capabilities() fell back to permissive(), so typed_get_vms() stopped requesting uuid; ha-unraid's VM UUID attributes have been empty since the dev server upgraded. The guard on the retired disk-removal mutation also always passed. Now:
    • GraphQL errors in 4xx bodies are kept on the raised UnraidAPIError, including after a POST redirect.
    • INTROSPECTION_DISABLED selects ServerCapabilities.introspection_disabled(): every current field except RETIRED_FIELDS, and not permissive.
    • Other introspection failures still fall back to permissive().

No library operation exceeds the new request limits (unraid/api#2121: 5,000 tokens, 100 aliases, cost 1,000, depth 20). All 72 argument-free read methods pass live on 4.37.9.

Follow-up for detecting this automatically: #136 (an upstream watch workflow).

Type of Change

  • Bug fix (non-breaking change fixing an issue)
  • New feature or endpoint support
  • Breaking change (change that modifies public API signatures or behavior)
  • Documentation update
  • Code refactoring (no functional changes)
  • Test additions or improvements
  • Dependency update

Pre-Submission Governance

  • I built and tested the package locally and verified this change actually works
  • I ran pytest locally and all tests pass
  • I ran linter / type checks locally and they pass
  • I pasted real local verification output under Testing Performed below (no placeholder text)
  • This PR is self-contained and is not a duplicate; I have not opened other overlapping or back-to-back PRs for the same change
  • If an AI agent created or assisted with this PR, a human reviewed and verified the changes before submission

Related Issues

Fixes #135

Changes Made

  • client.py:
    • query(): a response with errors and only null roots raises UnraidAPIError(errors=…). Partial results with at least one healthy root, and null roots without errors, are returned unchanged.
    • Readers: (result.get("docker") or {}) / (result.get("vms") or {}) in the nine readers (containers, containers safe, VMs, update statuses, port conflicts, raw containers, raw VMs, networks, logs).
    • 4xx bodies: _raise_for_graphql_rejection() keeps a 4xx response's GraphQL errors on direct and redirected POSTs. A body without GraphQL errors keeps the existing HTTP error <status> message.
    • Capabilities: get_capabilities() maps INTROSPECTION_DISABLED to ServerCapabilities.introspection_disabled().
  • capabilities.py: RETIRED_FIELDS (ArrayMutations.removeDiskFromArray) and ServerCapabilities.introspection_disabled().
  • tests/test_api_4378_compat.py (new, 25 tests): every affected reader raises UnraidAPIError on a null root; partial results and errorless null roots still pass; 4xx GraphQL errors are kept directly and after a redirect; non-GraphQL 4xx bodies are unchanged; capability selection for both introspection outcomes; VM uuid is requested; retired disk removal is refused without a request; 401/403 after a redirect stay UnraidAuthenticationError. tests/test_client_async.py: one existing mock response gets the status a real response always has.
  • README (capability paragraph) and CHANGELOG.

Python Package & API Architecture Standards

  • Type annotations present and compliant with PEP 561 (py.typed)
  • Async/await patterns followed where applicable (aiohttp/httpx)
  • Custom library exceptions raised (no raw swallowed errors or bare excepts)
  • No raw print() statements in package source code (uses standard logging)
  • Sensitive credentials (tokens, passwords, API keys) redacted from logs and __repr__
  • Not applicable (documentation-only or metadata change)

Testing Performed

  • Ran unit tests (pytest)
  • Ran linter (ruff or flake8)
  • Ran type checking (mypy)
  • Tested against live device or recorded fixtures
  • Not applicable (documentation-only or metadata-only change)

Test Results

$ uv run pytest --cov=src/unraid_api
721 passed, total coverage 96.25%
src/unraid_api/capabilities.py  100%
(client.py: every added line covered; the uncovered lines near the change are pre-existing)
$ uv run ruff check src tests && uv run ruff format --check .
All checks passed!
$ uv run mypy src
Success: no issues found in 7 source files
$ uv run pre-commit run --all-files
(all hooks Passed or Skipped)

Reproduction before the fix (mocked {"data": {"docker": null, "vms": null}, "errors": [...]}):
typed_get_containers -> AttributeError 'NoneType' object has no attribute 'get'
typed_get_vms        -> AttributeError 'NoneType' object has no attribute 'get'

Live, Unraid API 4.37.9+bedd403a (read-only):
before: INTROSPECTION: UnraidAPIError HTTP error 400: Bad Request; capabilities permissive: True
after:  INTROSPECTION: UnraidAPIError GraphQL request rejected (HTTP 400): GraphQL introspection is not allowed, but the current request is for introspection.
        permissive: False uuid: True removeDisk: False
        vm uuids: ['2d7a3a38-f9a6-9acb-bc3c-397b2ad8a644', '08e9bfa2-d0e7-fdf2-66e4-c035c04bc7a3']
        containers: 16
        all 72 argument-free get_*/typed_get_* methods: ok

Consumer check: ha-unraid (HA 2026.10.4 venv) against this branch, installed editable
1352 passed

Review: CodeRabbit is rate-limited, so Codex reviewed the branch locally in two passes.

  • Pass 1, fixed: a POST redirect raised before the 4xx body was read, so INTROSPECTION_DISABLED was missed on servers that redirect (SSL modes).
  • Pass 2, fixed: after a redirect, a 401/403 with a GraphQL body became a plain UnraidAPIError, so get_capabilities() would swallow a revoked key. The handler now leaves 401/403 to the authentication mapping.
  • Pass 3: no actionable findings.

Documentation

  • Docstrings added/updated where needed
  • README.md updated (if needed)
  • CHANGELOG.md updated under [Unreleased]
  • AGENTS.md / developer documentation updated (if architecture or process changed)
  • No documentation needed

Breaking Changes

None to signatures. Behaviour: on servers that refuse introspection, the client no longer uses permissive capabilities, and a query whose only roots come back null with errors now raises UnraidAPIError instead of returning {root: None}. That is what consumers already handled before 4.37.8, when such a response was data: null.

Checklist

  • I have updated CHANGELOG.md under [Unreleased] with details of this change
  • I linked the related issue in Related Issues (only maintainers may note "None", for housekeeping)
  • I completed all required sections in this template and removed placeholder-only content
  • My code follows the project's coding standards and Python best practices
  • I have performed a self-review of my own code
  • I have commented my code, particularly in hard-to-understand areas (if applicable)
  • My changes generate no new warnings
  • I have added tests that prove my fix is effective or that my feature works (if applicable)
  • New and existing tests pass locally with my changes
  • No sensitive information (tokens, passwords, personal data) is included

Disclosure: this PR was written with the help of AI (Claude Code). The "human reviewed" box above is left for the maintainer to tick after review.

Summary by CodeRabbit

  • Bug Fixes

    • GraphQL errors returned with HTTP 4xx responses are now reported with the associated error details.
    • Queries with entirely null results now correctly report GraphQL errors, while readers handle null service responses without raising an attribute error or presenting failed services as empty lists.
    • Compatibility with servers that disable schema introspection is improved. VM UUIDs continue to be requested, and retired disk-removal operations are refused without sending a request.
  • Documentation

    • Added guidance on introspection-disabled servers and how Docker and VM service failures are reported.

…on (#135)

- query() raises UnraidAPIError when every returned root is null with errors,
  and the docker/vms readers no longer raise AttributeError.
- Keep GraphQL errors from 4xx bodies (INTROSPECTION_DISABLED).
- get_capabilities() treats a server refusing introspection as current
  (minus RETIRED_FIELDS), so VM UUIDs are requested again.
Codex review: redirected POSTs raised before the body was read, so
INTROSPECTION_DISABLED fell back to permissive capabilities.
Codex review: a redirected 401/403 with a GraphQL body was raised as a plain
UnraidAPIError, so get_capabilities() swallowed a revoked key.
@coderabbitai

coderabbitai Bot commented Oct 30, 2026 •

Copy link
Copy Markdown
Contributor

Review in Change Stack →

Warning

Review limit reached

You've used all free OSS reviews for now. Wait for the free limit to reset to keep reviewing this public repository.

Next included review available in 50 minutes.

Check out review usage here.

View limit details

Limit details: You’ve used the included review currently available.

Learn how review limits work.

Review configuration:

Run configuration
  • Source mode: Auto
  • Repository: Unavailable
  • Branch: Unavailable
  • File: No configuration file from this repository applied
  • Commit: Unavailable
  • Layers: Central YAML (base), Organization UI (inherited)
  • Review profile: QUIET
  • Plan: Advanced
  • Run ID: 203906b9-964a-4b33-9b22-4b7845df295c
Commits

Reviewing files that changed from the base of the PR and between 9baffdf and bb8beca.

Files selected for processing: 2
  • CHANGELOG.md
  • src/unraid_api/_version.py
Walkthrough

The client now preserves GraphQL errors from eligible HTTP 4xx responses and treats all-null query roots with errors as complete failures. Result readers handle nullable Docker and VM roots. Capability discovery recognises disabled introspection and uses current capabilities that exclude RETIRED_FIELDS. Tests cover these behaviours, and the README and changelog document them.

Priority: Normal

Severity of issue fixed: Medium

Merge Risk: Minimal · previous review 9baff

The compatibility changes are ready to merge after normal checks.

Pre-merge checks: Passed 3
Check name Status Explanation
Linked Issues check Passed Issue #135 is directly linked and has coding requirements. The whole-PR diff implements nullable-root handling in query execution and the affected Docker and VM readers. It raises UnraidAPIError with …
Out of Scope Changes check Passed The changes stay within issue #135. The client changes address nullable Docker and VM roots, GraphQL error preservation, introspection-disabled capability selection, and authentication handling needed…
Tests Cover Changed Code Passed All changed source behaviour has PR test coverage. tests/test_api_4378_compat.py exercises nullable-root failures across all nine changed readers, partial and null-without-error query results, direc…
Full details: Linked Issues check

Explanation

Issue #135 is directly linked and has coding requirements. The whole-PR diff implements nullable-root handling in query execution and the affected Docker and VM readers. It raises UnraidAPIError with GraphQL errors when all returned roots are null, preserves partial results, and avoids treating a null root as a successful service result. It also parses GraphQL errors from 4xx responses, including redirected POST responses, maps INTROSPECTION_DISABLED to current capabilities excluding RETIRED_FIELDS, preserves permissive fallback for other introspection failures, restores VM uuid selection, and blocks the retired disk-removal mutation. The new compatibility tests cover these behaviours, including authentication errors after redirects. The reported test, lint, type-check, and live validation evidence supports the implementation.

Full details: Out of Scope Changes check

Explanation

The changes stay within issue #135. The client changes address nullable Docker and VM roots, GraphQL error preservation, introspection-disabled capability selection, and authentication handling needed for that flow. The capability constant, README, CHANGELOG, and compatibility tests document or verify the same behaviour. The existing mock-response status update supports redirect testing. No unrelated feature or behaviour is demonstrated.

Full details: Tests Cover Changed Code

Explanation

All changed source behaviour has PR test coverage. tests/test_api_4378_compat.py exercises nullable-root failures across all nine changed readers, partial and null-without-error query results, direct and redirected HTTP 4xx GraphQL rejection handling, redirected 401/403 handling, introspection-disabled and permissive capability paths, capability caching, VM UUID selection, retired disk refusal, and ServerCapabilities.has() behaviour. The updated redirect test in tests/test_client_async.py covers the ordinary successful redirect path. Documentation and constants require no dedicated tests.

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands.

@github-actions github-actions Bot added documentation Documentation changes bug Something is not working labels Oct 30, 2026
@codecov

codecov Bot commented Oct 30, 2026 •

Copy link
Copy Markdown

Codecov Report

❌ Patch coverage is 96.55172% with 2 lines in your changes missing coverage. Please review.

Files with missing lines Patch % Lines
src/unraid_api/client.py 95.74% 1 Missing and 1 partial ⚠️

📢 Thoughts on this report? Let us know!

@ruaan-deysel
ruaan-deysel marked this pull request as ready for review October 30, 2026 00:05
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

bug Something is not working documentation Documentation changes

Development

Successfully merging this pull request may close these issues.

[Bug]: Unraid API 4.37.8+: null docker/vms roots raise AttributeError, and disabled introspection drops VM UUIDs

1 participant