v0.38.0 — the exception's class no longer changes the status
A method that raised TypeError answered 400 with no X-VGI-RPC-Error. The dispatch paths caught (TypeError, pa.ArrowInvalid) after invoking the method, so the catch could not tell a caller's bad request from an exception the method body raised — and answered 400 to both.
It also caught _validate_result and _build_result_batch failures, which are server bugs, and reported them to the caller as their bad request. And because a 400 skips the 200 + error-flag path, the one signal separating a failure from a result went missing on a real failure — the defect 0.37.1 shipped TestErrorHeader to eliminate, surviving on a path that test did not reach.
What changed
Caller-controlled shape is now refused before dispatch, by _validate_call_signature, so everything the method raises past that point takes the ordinary error path: 200 + X-VGI-RPC-Error: true + EXCEPTION batch.
The check reads the Arrow schema the request batch arrived with, not the decoded Python values. The wire carries Arrow, the declared contract is an Arrow schema, and as_py() has already collapsed float64 against decimal128, timestamp against date, and string against large_string by the time kwargs exist. Type families are compared coarsely — within a family the declared type governs and Arrow's own cast handles the difference; unrecognised families are not policed, so an unfamiliar type passes through rather than being rejected.
Compatibility
Minor, not patch. The status for a method-raised TypeError changes from 400 to 200.
Visible to proxies, gateways, dashboards, and the access log's own http_status field — not to vgi-rpc clients, which read the EXCEPTION batch from the response body and ignore the status for everything but 401.
Conformance
TestErrorHeader is now parametrised over value/type/runtime errors, because the exception's class must not change the answer. Verified by sabotage: restoring the old catch fails exactly the raise_type_error case and leaves the other two passing.
All four ports already behaved this way and pass the strengthened group unchanged — Go 1143, Rust 995, Java 1132, TypeScript 1435. The reference was the only implementation answering 400.