Skip to content

SDK silently swallows non-2xx API responses (fail soft vs fail hard) #235

@bjornars

Description

@bjornars

Problem

Every resource write path (vis/group/job/repo — the create_* PUT and api_write POST) handles 404/403/409 explicitly but, on any other non-ok response, prints the body + headers + the literal "should raise a general error" and returns. The caller gets no exception and the write silently no-ops.

This surfaced via novem-code/gaia#2656: a paying user runs mail.bcc = [list]; when the list exceeds the subscription's external-address cap the server rejects the write, but the SDK swallows the response — mail.bcc = … returns with nothing stored and nothing to catch. (The server now returns a proper 400 with {message, rejected:[{line, reason}]}; the SDK still drops it on the floor.)

Minimal repro (novem 0.6.0, premium account, cap 25):

from novem import Mail
m = Mail("x", create=True)
m.bcc = [f"u{i}@example.com" for i in range(1, 31)]   # 30 > cap
# prints a blob incl. "should raise a general error", returns normally, stores 0

The tension: fail soft vs fail hard

Making the SDK raise on non-2xx is more correct, but it's a behavioral change for existing users, so it's worth a deliberate call:

  • Fail hard (raise): surfaces real errors (the #2656 case), but scripts that previously "succeeded" on a partial/failed write now get an exception — could break pipelines that quietly tolerated drops.
  • Fail soft (today): never raises on these paths → silent data loss, no signal. This is the reported bug.

Open questions:

  • Raise on any non-ok, or only on 4xx (keep 5xx softer / retryable)?
  • Keep the existing 409 carve-out (creating an object that already exists is intentionally a no-op).
  • Do we want an opt-in/opt-out (raise_on_error/strict flag, or a soft default + deprecation window) to avoid breaking existing users on a minor release?
  • Does this warrant a major version bump given the contract change?

Proposed direction

A shared raise_on_response() that surfaces the server message + any rejected lines and raises the matching NovemException subclass, wired into all write sites (keeping the 409 carve-out). Draft PR incoming.

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type
    No fields configured for issues without a type.

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions