A -32700 Parse error means the server received bytes that are not valid JSON, so it never validated jsonrpc, method, params, or id, and the error response always carries id null. The fault is therefore in transport or encoding, not in your application logic: truncated bodies, concatenated writes on a keep-alive connection, double-encoded payloads, missing Content-Encoding headers, non-UTF-8 bytes, and Content-Type mismatches are the usual causes. The reliable method is to log the exact bytes sent (length plus hash), the Content-Type and Content-Encoding actually applied, and the raw response body, then replay those bytes with curl so the failure is reproducible outside the application. Never retry a -32700 blind; fix the encoder or the proxy and rebuild the payload first. The JSON-RPC 2.0 specification fixes the code and the null id but does not fix the HTTP status, so read the HTTP layer and the error object together.
What -32700 Parse Error Actually Reports
The JSON-RPC 2.0 specification, Section 5.1.1, defines -32700 Parse error as the code returned when invalid JSON was received by the server. That single sentence carries the whole diagnosis: the server attempted to parse the request body as JSON, the parse failed, and execution stopped before any member of the request object was examined. No jsonrpc version check, no method lookup, no params validation, and no id extraction ever happened.
Because no request id could be detected, the error response carries id null. This is not a bug in the provider; it is the specified behaviour, and it is the strongest signal that you are looking at an encoding fault rather than an application fault. Compare it with a validation failure such as JSON-RPC -32602 invalid params validation, where the server did parse the envelope, did read your id, and echoes it back.
Section 6 of the same specification adds a batch-specific rule: if the batch body itself is not valid JSON, the server returns a single error object rather than an array of responses. An empty array is valid JSON and is a different case entirely, which is why batch-shaped queries often surface unrelated threads. For the ordering rules that apply once the body does parse, see JSON-RPC batching best practices.
- Documented: -32700 is returned when invalid JSON was received (JSON-RPC 2.0, Section 5.1.1).
- Documented: the error response uses id null because no request id could be detected.
- Documented: a batch whose body is not valid JSON yields one error object, not an array (Section 6).
- Documented: the specification does not mandate an HTTP status code for this condition.
Why Parse Failure Is a Transport and Encoding Fault
Application errors happen after the envelope is understood: a method that does not exist, a parameter of the wrong type, a contract call that reverts. A parse error happens before the envelope exists. The bytes on the wire are not a JSON document at all, so there is nothing for the node to route, meter, or authorise. This is why -32700 tickets that start with "my transaction failed" are usually mislabelled: no transaction was ever constructed.
The practical consequence is that you should stop reading your application code first and start reading the byte stream first. The question is not "what did my code intend to send" but "what did the socket actually carry". Everything in the ranked cause list below is a byte-level defect, and every one of them is reproducible once you capture the bytes.
This distinction also explains why the error object is thin. There is no data member with a stack trace, because the server has no context to describe. If you want to understand how code, message, and data normally relate, see Decoding the JSON-RPC error object.
Production Causes Ranked by Observed Frequency
The following ordering reflects how often each cause appears in real incident reports. Treat it as a triage order, not a statistical claim: start at the top and work down, because the first two causes account for the majority of cases where a payload looks correct in a debugger but fails in production.
Truncation is first because it is invisible in application logs. A proxy, load balancer, or client timeout can close the write mid-body, and the server receives a prefix of valid JSON that ends abruptly. Concatenation is second because retry logic frequently re-sends without discarding the first write, so a keep-alive connection carries two bodies back to back and the parser sees one invalid document.
The remaining causes are encoding defects: a payload double-encoded so the server receives a JSON string containing JSON rather than an object; binary or compressed bytes sent without the matching Content-Encoding header, so the server tries to parse gzip as UTF-8; a non-UTF-8 byte sequence injected by a string-concatenation JSON builder; and a Content-Type that does not match the body, such as form-encoded data POSTed to an endpoint that expects application/json. The HTTP Content-Encoding semantics documentation explains why a body encoded without the matching header is decoded as raw bytes rather than as the intended payload.
- Body truncated mid-write by a proxy, load balancer, or client timeout.
- Two bodies concatenated on one keep-alive connection after a retry that did not discard the first write.
- Payload double-encoded: the server receives a JSON string containing JSON instead of an object.
- Compressed or binary bytes sent without the matching Content-Encoding header.
- Non-UTF-8 byte sequence injected by manual string concatenation.
- Content-Type that does not match the body, such as form-encoded data sent to a JSON endpoint.
Capture-and-Diff Procedure for Turning a Vague Error into Evidence
A -32700 report is only actionable once you can state the exact bytes that were sent. Instrument the client at the last possible moment before the socket write, not at the point where you build the request object, because proxies and HTTP libraries can transform the body after your code is done with it.
Log four things together: the byte length of the serialised body, a hash of those bytes, the Content-Type and Content-Encoding headers actually applied to the request, and the raw response body exactly as received. The hash matters because it lets you prove that the bytes you replay are the bytes that failed, and the raw response body matters because a WAF may return HTML rather than a JSON-RPC error object.
Then replay the logged bytes with curl, including the same headers, so the failure is reproducible outside the application. If the replay succeeds, the defect is in your client stack or an intermediary; if the replay fails identically, you have the offending payload in hand and can bisect it byte by byte.
curl -sS -D - -o response.bin \
-X POST 'https://your-endpoint.example/rpc' \
-H 'Content-Type: application/json' \
--data-binary @captured-body.bin
# Inspect what came back, byte for byte
wc -c captured-body.bin response.bin
head -c 400 response.bin; echo
# Confirm the captured body is valid JSON before blaming the server
node -e "const fs=require('fs');const b=fs.readFileSync('captured-body.bin');try{JSON.parse(b.toString('utf8'));console.log('body parses locally')}catch(e){console.log('local parse failure:',e.message)}"Runnable Node.js Example: Assert Before You Send
The cheapest permanent fix is a pre-flight assertion in the client. Serialise with JSON.stringify, parse the result back, and confirm you get an object rather than a string. A double-encoded payload passes a naive truthiness check but fails this round-trip test immediately.
The example below also prints the raw error payload, including the null id, so you can see the difference between a parse failure and an application error in the same log stream. Run it against any endpoint you control, including one from the Ethereum network page or your own API service deployment.
const endpoint = process.env.RPC_URL || 'https://your-endpoint.example/rpc';
function buildBody(method, params) {
const body = JSON.stringify({ jsonrpc: '2.0', id: 1, method, params });
const roundTrip = JSON.parse(body);
if (typeof roundTrip !== 'object' || roundTrip === null || Array.isArray(roundTrip)) {
throw new Error('Body did not round-trip to a JSON object; check for double encoding');
}
return body;
}
async function call(method, params) {
const body = buildBody(method, params);
const bytes = Buffer.byteLength(body, 'utf8');
const hash = require('crypto').createHash('sha256').update(body).digest('hex').slice(0, 16);
console.log('sending bytes=%d sha256=%s content-type=application/json', bytes, hash);
const res = await fetch(endpoint, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body
});
const raw = await res.text();
console.log('http status=%d raw=%s', res.status, raw.slice(0, 400));
let parsed;
try {
parsed = JSON.parse(raw);
} catch (e) {
console.error('Response is not JSON; the HTTP layer is the real signal here');
return;
}
if (parsed.error) {
console.error('rpc error code=%s message=%s id=%s', parsed.error.code, parsed.error.message, parsed.id);
if (parsed.error.code === -32700) {
console.error('Parse failure: the server never read a request object. Fix the encoder or proxy, then rebuild the payload.');
}
} else {
console.log('result=%o', parsed.result);
}
}
call('eth_blockNumber', []).catch((e) => console.error('transport failure:', e.message));Separating a Client Parse Error from a Server Parse Error
The most misleading -32700 tickets are the ones where the reader's own parser is at fault. If the response body is not valid JSON, your client cannot decode it, and the resulting exception is often reported as "the node returned a parse error" when in fact the node returned HTML, an empty body, or a truncated stream. Read the exchange in both directions before assigning blame.
The test is simple and mechanical: parse the raw response body yourself. If it parses and contains an error object with code -32700, the server rejected your request bytes. If it does not parse, the fault is on the response path, and the correct signal is the HTTP status and headers, not the JSON-RPC error object.
This is also where provider behaviour varies. Documented JSON-RPC semantics are fixed by the specification, but the HTTP status used for a parse failure is not, and some providers front their endpoints with a web application firewall that answers with an HTML challenge page. In that case the error object must be ignored entirely.
- Response parses and contains code -32700: the server rejected your request bytes.
- Response does not parse: the fault is on the response path, so read HTTP status and headers.
- Response is HTML: an intermediary such as a WAF answered, and the JSON-RPC error object is absent.
- Response is empty: suspect truncation or a connection reset rather than a JSON defect.
Retry Semantics: Why Blind Retries Cannot Succeed
A -32700 is deterministic with respect to the bytes that caused it. The same bytes will fail to parse on the next attempt, and on the attempt after that, so a retry loop that re-sends an unchanged buffer converts a single failure into a sustained error rate and can amplify load on the endpoint. This is the opposite of a transient network fault, where retrying is the correct response.
The correct action is to fix the encoder or the intermediary and then rebuild the payload from scratch. Only after the body has been regenerated, re-serialised, and re-asserted should a retry be issued. If your retry logic lives in a shared HTTP client, add a guard that refuses to retry when the response contains code -32700.
For comparison, a transient internal failure such as JSON-RPC -32603 internal error debugging may legitimately be retried with backoff, because the request was understood and the failure occurred downstream. The distinction is whether the server ever read your request object.
Results Table: Reproducible Evidence from Your Own Endpoint
Build a table with one deliberately corrupted payload per row and run it against your own endpoint. This converts an anecdotal error into a reproducible matrix, and it also reveals how your specific provider maps parse failures onto HTTP status codes, which the specification leaves open.
Fill in the columns below with values you observe. Do not copy numbers from any article, including this one: the point of the exercise is that the evidence comes from your endpoint, your proxy chain, and your client stack.
- Corruption applied: truncated body, concatenated bodies, double-encoded string, gzip without Content-Encoding, invalid UTF-8 byte, form-encoded body.
- Server code: the JSON-RPC error code observed, expected to be -32700 for the parse cases.
- Message: the exact message string returned, quoted verbatim.
- HTTP status: the status line observed, which may be 400, 200, or 500 depending on the provider.
- id echoed: the id member in the response, expected to be null for parse failures.
- Elapsed ms: wall-clock time from write to full response read, measured by your client.
| Corruption applied | Server code | Message | HTTP status | id echoed | Elapsed ms |
| --- | --- | --- | --- | --- | --- |
| ____ | ____ | ____ | ____ | ____ | ____ |
| ____ | ____ | ____ | ____ | ____ | ____ |
| ____ | ____ | ____ | ____ | ____ | ____ |
| ____ | ____ | ____ | ____ | ____ | ____ |Limitations and Tradeoffs in Parse-Error Diagnosis
The specification deliberately does not fix the HTTP status code for a parse failure. Some endpoints answer 400, some answer 200 with an error object in the body, and some answer 500. Any client that keys its error handling solely on HTTP status will misclassify at least one of these, so handle both layers explicitly.
Provider behaviour also varies at the edge. A web application firewall or gateway may intercept a malformed body before it reaches the JSON-RPC handler and return HTML, a redirect, or a challenge page. In that situation the JSON-RPC error object does not exist and must not be synthesised; the HTTP layer is the only reliable signal.
Finally, byte-level capture has its own tradeoffs. Logging full request bodies can expose sensitive parameters and inflates log volume, so prefer logging length plus hash in production and retain full bodies only in a controlled debugging window. The hash is enough to prove that a replay matches the original failure.
Troubleshooting Checklist for a Live -32700 Incident
Work the checklist in order and stop as soon as the replay reproduces or clears the failure. Each item is designed to eliminate a layer rather than to guess at a cause.
If the replay succeeds with identical bytes and headers, the defect is in your client stack or an intermediary between client and endpoint. If it fails identically, bisect the captured body: cut it in half, test each half, and continue until the offending byte range is isolated. A single stray byte is enough to invalidate the whole document.
- Confirm the captured body parses locally with a strict JSON parser before contacting the server.
- Confirm Content-Type matches the body and Content-Encoding matches the actual encoding.
- Confirm no proxy is buffering, rewriting, or truncating the body; check its body-size limit.
- Confirm retry logic discards the previous write before re-sending on a keep-alive connection.
- Confirm the response body is JSON before interpreting any error code inside it.
- Confirm the endpoint URL and method are correct; a GET to a POST-only endpoint can surface as a parse error at some gateways.