Skip to main content

First things first: the raw error appears exactly once, in the response body

APIYI returns error details only through the API response body. The backend log is a billing ledger — it records calls that produced a charge. Failed requests are neither billed nor listed there.So “I can’t find it in the backend log” does not mean “it didn’t happen.” It means the only record of that error lives in your client. If you didn’t print it and persist it, it is gone for good — and we can’t recover it either.
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:
That string is what the client framework produced after wrapping the error. It preserved the model name, HTTP method, URL and status code — and threw away the one thing that mattered, the response body. The best answer support could give was:
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:
Same 400, three completely different actions. Discarding the response body turns a three-way decision into guesswork — and a wrong guess costs a round trip of support messages plus a retry you never needed.Worse: two of those three cases should never be retried at all. If you can’t tell them apart, blind retrying is your only option, burning both time and quota.

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.
Read it in reverse and it becomes the strongest single test for connection problems: if the log does contain a billing entry, the request reached the upstream and consumed resources; if it doesn’t, the problem almost certainly occurred before reaching the upstream (network, authentication, parameter validation). Full details in Reading billed amounts in the 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

Don’t call raise_for_status() before reading the body. The HTTPError it raises carries only 400 Client Error: Bad Request for url: ..., while the real message sits untouched in resp.text with nobody reading it — which is one of the ways the case at the top of this page happens. If you do use it, pull resp.text out first.

Python / OpenAI SDK

The official SDK already attaches all three pieces to the exception object. Most people just print their own one-line message instead:
Even a one-liner should be print(f"API error: {e}") rather than print("request failed") — the SDK exception’s str(e) already contains the server’s message. What actually destroys information is throwing the exception object away entirely.

Node.js

With the SDK:
With plain fetch, this is where it most often goes wrong:
That 400 Bad Request from POST https://api.apiyi.com/v1/images/edits at the top of this page is literally ${resp.status} ${resp.statusText} from ${resp.method} ${resp.url}the body was never read at all.fetch does not reject on HTTP-level errors; it just sets resp.ok to false. Throwing resp.statusText at that moment discards the body along with the response object. Always await resp.text() before you throw — that single line is the difference between a diagnosable report and an unanswerable one.

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:
  • -i prints response headers, which is where x-request-id lives;
  • -sS hides the progress bar but keeps error output;
  • -w appends 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:
It is far uglier than 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 reading resp.text first — the message is still in memory, just never retrieved;
  • if (!resp.ok) throw new Error(resp.statusText) in fetch — 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.message points 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_failed or 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_id is the only anchor that reconciles precisely.

Support ticket template (copy-paste)

WeCom Support

WeCom support QR codeScan the code, or click this card to reach WeCom support.You can also reach us on Telegram at @apiyi001 or by email at [email protected].
Send the template above as text — it is far more efficient than any description. With a request_id we can go straight to the full trace of that single call, instead of asking “roughly what time, and which model?” See Log query API for how to look up request_id.

API Manual

Common error codes, authentication and rate limits

Connection drops

Full troubleshooting path for ECONNRESET, errno 10054 and SSL EOF

Log query API

Pull call logs via API — how to look up request_id and reconcile billing

Reading 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