Skip to content

fix(api): raise AtomicAPIError on non-dict job responses instead of Pydantic ValidationError #11

Description

@mrrobot47

Summary

SitesClient has 8 call sites that do Job.parse_obj(response_data) with no guard on the response shape. If the API returns anything that isn't a dict with a job_id (e.g. an empty list []), callers get a cryptic Pydantic ValidationError instead of a meaningful AtomicAPIError. Make the failure mode actionable.

What triggered this

A live client.sites.update_domain(domain=…, new_domain="...", keep_old_domain=True) call surfaced:

ValidationError: 1 validation error for Job
Input should be a valid dictionary or instance of Job
[type=model_type, input_value=[], input_type=list]

i.e. data: [] came back from the API instead of the documented {job_id, wpcom_blog_id, atomic_site_id, domain_name, old_domain_name} shape. Root cause (API regression, undocumented no-op shape, or error body on a 200) is unknown — because the SDK discarded the HTTP response before it could be inspected. That's the bug to fix, regardless of the API's behaviour.

Call sites affected

All in atomic_sdk/api/sites.py:

  • line 125 (create_site)
  • line 144 (delete_site)
  • line 167 (update_domain)
  • line 264, 284, 306, 331, 404 (various software/meta/options ops)

All 8 have the pattern:

response_data = self._post(endpoint)
job = Job.parse_obj(response_data)
job._client = self._client
return job

Scope

1. Add a helper on SitesClient

def _parse_job(self, response_data: Any) -> Job:
    """Parse a job-style response, raising AtomicAPIError (not ValidationError) on shape mismatch."""
    if not isinstance(response_data, dict) or "job_id" not in response_data:
        raise AtomicAPIError(
            message=(
                f"Expected a job response (dict with 'job_id'), "
                f"got {type(response_data).__name__}: {response_data!r}"
            ),
            status_code=200,
        )
    job = Job.parse_obj(response_data)
    job._client = self._client
    return job

2. Apply across all 8 call sites

Replace each job = Job.parse_obj(response_data); job._client = self._client with job = self._parse_job(response_data).

Acceptance criteria

  • client.sites.update_domain(...) against a response where data == [] raises AtomicAPIError with a message containing the actual payload — not pydantic.ValidationError.
  • The other 7 call sites behave the same (same helper, same assertion).
  • A real successful data: {job_id: 1234, ...} response still returns a valid Job instance with _client attached.
  • Helper is a method on SitesClient — not duplicated inline, not copied across files.

Out of scope

  • Investigating why the API returned [] on that one call (separate question, not an SDK concern).
  • Extending the same guard to other resources (BackupJob parsing, etc.) — do those as separate follow-ups if they surface similar issues.
  • Retry on "looks like an error body on a 200" — unrelated; if it's really a 500/429 it belongs in the retry/backoff story.

References

  • Observed failure trace: atomic_sdk/api/sites.py:167pydantic/main.py:1222.
  • Job model: atomic_sdk/models.py:42.

Metadata

Metadata

Assignees

Labels

No labels
No labels

Type

No type

Projects

No projects

Milestone

No milestone

Relationships

None yet

Development

No branches or pull requests

Issue actions