Guidance for Claude Code when working in this repository.
A demonstration XML-RPC client/server in Python with two additions on top of stock xmlrpc: keyword-argument support, and exact transport of integers/floats that XML-RPC's native types cannot represent. Four files carry all the logic — server.py, client.py, utils.py, plus the tests.
pip install -r requirements.txt
python server.py both # HTTP :8000 + HTTPS :8443
python server.py http --port 9000 # single protocol; --host/--port
python server.py https # generates server.crt/.key on first run
python client.py add 2 2 # add/subtract/multiply/divide
python client.py list-methods --url https://localhost:8443
python client.py test # connection check
pytest tests/ # unit tests always run; e2e tests
# skip unless servers are listening
LOG_LEVEL=DEBUG python client.py add 1 1 # both log sinks honour LOG_LEVELRunning the full e2e suite requires python server.py both in another terminal.
XML-RPC has no keyword arguments and no way to express a "response object", so every non-system call is wrapped in a Data (utils.py).
Request path:
DataServerProxy._Method.__call__(client.py) packs the call's*args/**kwargsinto aDataand sends it as the single XML-RPC parameter.ThreadedXMLRPCServer._dispatch(server.py) recognises the dict by its_is_data_objectmarker, unpacks and decodes it, and calls the target function.- The return value is wrapped in another
Datacarryingresponse_code(200/400/404),resultanderror. XMLRPCClient._return_resultdecodes it, raisingRPCErroron a server-reported error.
Data is marshalled by making it look like a dict: __dict__ is overridden as a property returning _get_dict_data(), which is what xmlrpc.client's dump_instance reads. That property override is load-bearing — do not replace it with a plain attribute. _get_dict_data() also strips None values, so error/result keys may be absent; always read them with .get().
XML-RPC's <int> is 32-bit and <double> is a lossy float, so numbers travel as tagged strings:
| Python value | On the wire |
|---|---|
42 |
"__INT__42" |
3.14 |
"__FLOAT__3.14" |
"__INT__42" (a real string) |
"__STR____INT__42" |
True / False |
unchanged (checked before int, since bool subclasses int) |
convert_value_for_xmlrpc encodes, convert_value_from_xmlrpc decodes; both recurse through lists, tuples and dicts.
The __STR__ escape is required for correctness. Without it a user string that happens to start with __INT__ decodes into an integer on the far side. Any change to the marker set must keep encode/decode symmetric — tests/test_conversion.py asserts the round trip.
ThreadedXMLRPCServer is a ThreadingMixIn server, so each request is handled on its own thread.
The HTTPS path deliberately does not wrap the listening socket. Doing so makes SSLSocket.accept() run the TLS handshake inside the single accept loop, where one client that connects and stays silent blocks every other client — a one-line denial of service. Instead the context is passed to the server and get_request() wraps each accepted connection with do_handshake_on_connect=False, deferring the handshake to the first read on the worker thread. BoundedXMLRPCRequestHandler.timeout then bounds how long a silent client can hold that thread. Preserve this arrangement.
- Never dispatch underscore-prefixed method names.
SimpleXMLRPCServerblocks them inresolve_dotted_attribute, but the custom_dispatchbypasses that path; the explicit guard in_dispatchis what stops__dir__,__repr__and friends from being remotely callable.register_instanceexposes every public attribute ofMathFunctions, so this check is the only boundary. - Server errors become
Data(response_code=400), not XML-RPC faults._dispatchcatches everything. Callers must checkresponse_code, and CLI paths must map failures to a non-zero exit. - Keep encode/decode symmetric (see wire markers above).
- The private key is written
0600via_write_secret. Don't switch it back to a plainopen().
tests/test_conversion.py— pure unit tests for conversion andData.tests/test_client.py— end-to-end; the session-scopedhttp_url/https_urlfixtures skip when no server is listening. Do not reintroducepytest.exithere: it aborts the entire run, taking the server-independent tests with it.conftest.pyat the repo root puts the root onsys.path; without it plainpytest tests/fails withModuleNotFoundError.
Datavalidates arguments withjson.dumps, but the transport is XML-RPC. That rejectsbytesanddatetime, which XML-RPC handles natively withuse_builtin_types=True. Intentional conservatism, but it is stricter than the wire format requires.- No authentication, and the client disables certificate verification by default. Local demo only — see the security notes in
README.md. logs/,server.crtandserver.keyare generated at runtime and gitignored.