A 502 in your PDF-to-LLM pipeline is the gateway, not the model
- Calling AI from Business Central: the platform realities nobody warns you about
- A 502 in your PDF-to-LLM pipeline is the gateway, not the model
- Shipping a Copilot feature in Business Central that survives real users
- Your AI feature must run on a fresh tenant, or it doesn't run
Everyone is wiring document AI into Business Central right now — invoice reading, the GA Payables Agent, your own PDF-to-LLM extension. You build one: an AL extension grabs a PDF attachment, base64-encodes it, and POSTs it through an AI gateway to a large language model. It works for a plain text email. You feed it a real invoice PDF and get back a flat 502 Bad Gateway. No model output, no useful error — and you spend the next hour rewriting a prompt the model never read.
That hour is wasted, and here is the thesis: a 502 is a transport verdict, never a model verdict. The request died between the gateway and the upstream provider — the model never saw your document at all. You cannot have a prompt problem with a request that never reached the prompt, so the instant you see a 502, stop touching the prompt and start bisecting the pipeline.
Bisect the pipeline, don’t theorize
The fastest path to the real cause is mechanical. Three checks, in order, each one cutting the search space in half:
- Send the same request without the PDF — text-only payload. If that succeeds, the problem is the document leg specifically. Not auth, not the prompt, not the model, not the gateway in general. You have just eliminated four suspects with one call.
- Shrink the document. A one-page text PDF versus a scanned multi-page one tells you whether you are fighting size/timeout or the type of content. Different fixes; cheap to find out.
- Read the gateway’s own logs, not just BC’s response. This is the step people skip. BC only ever sees the 502 — the terse outer verdict. The gateway saw why the upstream rejected or timed out. The answer is almost always sitting in those logs, unread.
In the case that cost me an afternoon, the upstream provider could not ingest the raw PDF binary the way the gateway forwarded it. It failed upstream, and the gateway surfaced a bare 502 with none of that context.
The fix: force a text-extraction path
The key realization: the model does not need the PDF bytes. It needs the text. Passing the binary straight through asks the provider to do PDF handling that it may not do over that path at all. So do the extraction yourself, at the gateway, and send clean text:
{
"model": "…",
"plugins": [
{ "id": "file-parser", "pdf": { "engine": "pdf-text" } }
],
"messages": [
{ "role": "user", "content": "Extract the invoice header fields as JSON." }
]
}
Same model, same prompt — but now it receives extracted text and responds, instead of 502-ing on a binary it could never consume.
Status codes are a map of where it broke
Once you stop reading the status as a generic “it failed” and start reading it as a location, the debugging gets fast. A rough map for this kind of pipeline:
400— your request is malformed. The gateway looked at it and refused. Fix the payload, not the model.413— payload too large. The document (often the base64 binary) blew a size limit. This is the one that screams “extract text instead of shipping bytes.”429— rate limited. Transport and payload are fine; you are calling too fast. Back off.500— the upstream itself errored on a request it did receive. Now the prompt or the content is genuinely in play.502/504— the request died in transit between gateway and upstream (bad gateway / timeout). The model never saw it. This is the case in this post.
The dividing line that matters: did the upstream receive the request or not? 502/504 say
no — stop debugging the prompt. 500 says yes — now the prompt is fair game.
When pdf-text is not enough: scanned documents
Text extraction assumes the PDF has a text layer. A scanned invoice — an image wrapped in a
PDF — does not. Run pdf-text over it and you get clean, confident, empty output, and a model
that hallucinates fields from nothing because you handed it a blank page.
So the bisection from earlier has a second branch: if shrinking to a text PDF fixes it but the real documents are scans, your problem was never transport — it is that you need an OCR step, not a text-extraction one. Same architectural move (do the work at the gateway, send text to the model), different engine. The failure looks identical from BC; the fix is one layer over.
The parts that generalize
Beyond this one bug, three durable lessons:
- A 502 is an infrastructure verdict, not a model verdict. It is a statement about transport between two machines. Stop debugging the prompt the instant you see one.
- PDFs are not a native content type for LLMs through every path. Decide explicitly where extraction happens — and make it explicit — rather than hoping the binary survives the trip.
- The default model and the default plugins are often tenant/config, not code. Two environments behaving differently on identical AL is your signal to look at the gateway configuration, not the extension. This is exactly why the gateway is the right place to own this behavior.
If your BC document-AI feature works in the demo and 502s on the first real PDF, that is not a bug in your prompt. It is the binary never reaching the model. Fix the transport, and the “intelligence” you were debugging turns out to have been fine all along.
Part two of the production-AI series. The platform guardrails were part one.