One-line takeaway: print the raw response body exactly as returned; don’t keep only the one-line string your program wrapped it into. A string like
400 Bad Request contributes almost nothing to diagnosis — the real answer is in the JSON it discarded.A real case: 400 Bad Request tells you nothing
A customer reported exactly one line:A 400 is usually either content safety or a parameter problem. Most likely content safety.That is a guess, not a conclusion. Because for that very same call, the actual response body could have been any of the following three — and each calls for a completely different action:
What the backend log does and doesn’t have
The mental model to get right first: the backend log is a billing ledger, not an error log.The 7 fields you must keep
This is everything needed to diagnose one failed call. Missing any of them degrades diagnosis back into guesswork:Don’t truncate the response body. Truncating to 200 characters is reasonable for routine business logs, but for diagnosis the useful detail is often at the end. Keep at least the first 2000 characters. If you’re worried about base64 flooding your logs on image endpoints, print the full body only when
status_code >= 400 — error bodies are short anyway.Correct error-capture patterns
There is really only one principle: catch at two layers, and discard nothing at either layer.- Transport-layer failures: connection reset, TLS handshake failure, timeout, DNS failure. There is no HTTP response at all — the exception text is all you get.
- HTTP-layer errors: the server returned 4xx / 5xx. There is a response body, and you must read it.
Python / requests
Python / OpenAI SDK
The official SDK already attaches all three pieces to the exception object. Most people justprint their own one-line message instead:
Node.js
With the SDK:fetch, this is where it most often goes wrong:
Reproducing with cURL
When you ask someone to reproduce an issue, this command is the least effort — it surfaces status code, headers, body and timing in one shot:-iprints response headers, which is wherex-request-idlives;-sShides the progress bar but keeps error output;-wappends the status code and total time, handy for comparing against your timeout settings.
Wrappers and in-house gateways
A good example
This error came from a customer’s ComfyUI node:400 Bad Request — and yet it is complete, so the direction is settled in seconds:
The conclusion follows immediately: this is a transport-layer problem, unrelated to content safety or parameters, and it produces no charge (the request never completed). Troubleshooting path: Image API connection drops.
Compare the two: one is neatly packaged and explains nothing (
400 Bad Request); the other is long and ugly and points straight at the root cause (errno 10054). For diagnosis, a raw, ugly, complete error beats a friendly, tidy, rewritten one every time.Where to find the raw output in common tools
Three rules for an in-house gateway
1
Pass through, never rewrite
A middle layer may append context (which service, which tenant, which retry attempt), but it must not replace the upstream
error.message. Once rewritten, there is no second place to recover the original from.2
Store the user-facing message separately from the raw one
Follow the three-part structure used in Gemini image error handling:
userMessage (friendly copy for end users), devMessage (your classification for developers), and rawResponse (the response body, unmodified). Polish the first two freely; store the third verbatim.3
Never emit an unknown error
In your fallback branch, record
status, x-request-id and the first 2000 characters of the body. An “unclassified error” that carries the original text is diagnosable; a clean “unknown error” is not.Anti-patterns that make diagnosis impossible
except Exception as e: print("request failed")— the exception object is gone, and you don’t even know which layer failed;- Recording the status code but not the body — exactly the case at the top of this page;
- Calling
raise_for_status()without readingresp.textfirst — the message is still in memory, just never retrieved; if (!resp.ok) throw new Error(resp.statusText)infetch— the body is discarded with the response object;- Leaving only a clean 200 after a successful retry — log every attempt separately, otherwise you never see how many times the transport failed, and you may mistake your own retries for gateway behavior;
- Logging to stdout only, or rotating daily with overwrite — by the time a customer reports the problem, the original record has usually scrolled away;
- Reporting an issue with a phone photo of the screen — paste the text instead; screenshots regularly cut off half a line of the error.
When to contact support
Work through the capture and interpretation steps above first. If any of the following holds, bring your material to support:- You have the full response body and
error.messagepoints upstream (upstream_error, a raw upstream 5xx, or an explicit channel error); - The same request parameters work on a different model or at a different time, and only one specific model fails consistently;
- The error is
500+write_response_body_failedor a similar downstream-delivery failure, and it reproduces consistently (these are not billed; see Connection drops); - You suspect billing doesn’t match your actual calls —
request_idis the only anchor that reconciles precisely.
Support ticket template (copy-paste)
WeCom Support

@apiyi001 or by email at [email protected].Related documentation
API Manual
Common error codes, authentication and rate limits
Connection drops
Full troubleshooting path for
ECONNRESET, errno 10054 and SSL EOFLog query API
Pull call logs via API — how to look up
request_id and reconcile billingReading billed amounts
Why failed calls never reach the log, and how to use that as a diagnostic test
Timeout configuration
Timeout tiers per model type, and what to check when raising it doesn’t help
Image API essentials
Synchronous calls, base64 prefix differences,
400 invalid_image_file preprocessing