``You're calling OpenAI from Node.js. 99% of requests work. Then randomly:
BadRequestError: 400 could not parse JSON body
Or:
SyntaxError: Invalid JSON: EOF while parsing an object
Or, if you're using tool calling:
Invalid JSON in tool call arguments
You check your code. Nothing changed. You retry manually — it works. You deploy again. It breaks again.
What's actually happening
These errors are NOT your bugs. They're transient artifacts from:
- Proxy / network corruption — the request body arrives at OpenAI partially mangled. OpenAI returns 400.
- Streaming truncation — the SDK parses JSON before the stream signals completion. Especially with max_output_tokens set low.
- Model-side tool call malformation — the model emits single quotes, trailing commas, or markdown code fences inside function.arguments.
Every one of these is intermittent. So you can't debug it with a stack trace. You just suffer.
The wrong fix
People usually do this:
try {
const res = await client.chat.completions.create({...});
} catch (e) {
const res = await client.chat.completions.create({...});
}
Problems:
- You retry errors that should NOT be retried (invalid API key, rate limit that needs backoff, context length exceeded).
- You don't sanitize tool call args, so malformed JSON still crashes downstream.
- You add 20 lines of retry boilerplate per call site.
The right fix: classify then act
Different errors need different handling:
| Error | Correct action |
|---|---|
| Transient 400 (could not parse JSON body) | Retry with backoff + jitter |
| Truncated stream (EOF while parsing) | Retry, possibly with higher max_tokens |
| Malformed tool args | Sanitize inline, no retry needed |
| Invalid API key | Fail fast, do NOT retry |
| Rate limit | Retry with longer backoff |
Doing this manually is ~150 lines of code you'll write badly. Or:
npm install @coder12-z/llm-shield
import { shield } from "@coder12-z/llm-shield";
import OpenAI from "openai";
const client = new OpenAI();
const safeCall = shield(async (prompt) =>
client.chat.completions.create({
model: "gpt-4o-mini",
messages: [{ role: "user", content: prompt }],
response_format: { type: "json_object" },
}),
{
maxRetries: 3,
onRetry: ({ attempt, kind }) => console.log(`retry #${attempt} (${kind})`),
}
);
Three lines of config. Zero dependencies. Works with OpenAI, Anthropic, Gemini, or anything that throws Error objects with status and message.
What it doesn't do
- It doesn't repair arbitrary JSON strings — for that, use jsonrepair (https://www.npmjs.com/package/jsonrepair).
- It doesn't call LLMs for you — it wraps your existing calls.
- No API key. No network calls. No telemetry.
Links
- npm: https://www.npmjs.com/package/@coder12-z/llm-shield
- GitHub: https://github.com/zey-netizen/llm-shield
- Landing: https://zey-netizen.github.io/llm-shield/
MIT licensed. Feedback welcome.
Top comments (0)