Skip to content

Raise ServerError for any JSON-RPC 500 error body - #161

Open
jplfaria wants to merge 1 commit into
ModelSEED:devfrom
jplfaria:fix/rpcclient-non-dict-error-body
Open

jplfaria wants to merge 1 commit into
ModelSEED:devfrom
jplfaria:fix/rpcclient-non-dict-error-body

Conversation

@jplfaria

@jplfaria jplfaria commented Nov 6, 2026

Copy link
Copy Markdown
Contributor

Why this matters in our workflow

We annotate genomes with RAST, and RAST is reached through RPCClient: RastClient uses it, and so does the
ModelSEED API. When the server returns an HTTP 500, callers should get a ServerError with the server's message.
Instead, for several common body shapes they get a TypeError raised from inside rpcclient.py. The string case has
happened in production: on 30 Jun 2026 the ModelSEED API's failure watcher logged it twice, from its RAST annotation
step (ModelSEED/modelseed-api-ops #18 and #19).

What happens

rpcclient.py L71-L79 at 062714b
does raise ServerError(**err["error"]). That works only when the error is an object whose keys are exactly
ServerError's parameters. Other shapes raise TypeError on that line:

500 response body dev raises
{"error": "service unavailable"} TypeError: ... argument after ** must be a mapping, not str
{"error": {"code": -32601, "message": "Method not found"}} (JSON-RPC 2.0, no name) TypeError: ServerError.__init__() missing 1 required positional argument: 'name'
{"error": {"name": "Err", "code": 1, "message": "m", "stack": "..."}} TypeError: ServerError.__init__() got an unexpected keyword argument 'stack'
"internal error" (a JSON string, not an object) TypeError: string indices must be integers

How to reproduce

import modelseedpy.core.rpcclient as rc

class Resp:
    status_code = 500; ok = False; headers = {"content-type": "application/json"}
    text = '{"error": {"code": -32601, "message": "Method not found"}}'
    def json(self): return {"error": {"code": -32601, "message": "Method not found"}}

rc._requests.post = lambda *a, **k: Resp()
rc.RPCClient("http://example.invalid").call("Svc.method", [])
# dev: TypeError: ServerError.__init__() missing 1 required positional argument: 'name'

The fix

                 err = ret.json()
-                if "error" in err:
-                    raise ServerError(**err["error"])
+                error = err.get("error") if isinstance(err, dict) else None
+                if isinstance(error, dict):
+                    raise ServerError(
+                        error.get("name", "Unknown"),
+                        error.get("code", 0),
+                        error.get("message"),
+                        error.get("data"),
+                        error.get("error"),
+                    )
                 else:
                     raise ServerError("Unknown", 0, ret.text)
  • An error object fills ServerError from its keys, with defaults for missing ones and extra keys ignored.
  • Anything else takes the existing "Unknown" branch with the response text, as a 500 without an error key already
    does.
  • A complete error object gives the same ServerError as before.
  • RPCClient has one caller inside the package, RastClient (rast_client.py:57). For it, only the exception type of
    a failing call changes, from TypeError to ServerError.

Tests

This adds a new file, tests/core/test_rpcclient.py (there were no RPCClient tests). It monkeypatches
requests.post and checks:

  • test_call_server_error: a complete error object keeps its name, code and message. This locks in the current
    behaviour, and it passes on dev.
  • test_call_server_error_other_shapes, parametrized over the four bodies in the table: each raises a ServerError
    with the expected name, code and message.

Against dev's code, the four other-shapes cases fail and test_call_server_error passes:

E                   TypeError: ServerError.__init__() missing 1 required positional argument: 'name'
E                   TypeError: ServerError.__init__() got an unexpected keyword argument 'stack'
E                   TypeError: modelseedpy.core.rpcclient.ServerError() argument after ** must be a mapping, not str
E                   TypeError: string indices must be integers
4 failed, 1 passed

With the fix, all 5 pass. Full suite (Python 3.10, cobra 0.32.1): tests/core goes from 56 to 61 passed, and
tests/ from 58 to 63. black 22.8.0 and the other pre-commit hooks pass.

Related

🤖 Generated with Claude Code

RPCClient.call did ServerError(**err["error"]). That raised TypeError
instead of ServerError, hiding the server's message, whenever the error
body was not an object with exactly ServerError's keywords:
- a string error: "argument after ** must be a mapping, not str" (seen in
  production from the RAST genome_annotation service at
  tutorial.theseed.org, the endpoint RastClient uses);
- a JSON-RPC 2.0 error object, which has no "name": "missing 1 required
  positional argument: 'name'";
- an error object with another key: "unexpected keyword argument";
- a JSON body that is not an object: "string indices must be integers".

Build ServerError from a dict error with .get() defaults ("Unknown", 0),
and use the existing "Unknown" ServerError with the response text for
anything else, as for a 500 without an "error" key.

Tests: a full error object, and the four shapes above.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
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.

1 participant