My PDF parser failed on the first two calls of every fresh process. My error dashboard showed zero.
I run a resume builder for Indian job seekers. The first thing a new account does is upload the resume it already has. Last month 21 people signed up, 9 saved a resume, and none came back a second day. My usage table sho
I run a resume builder for Indian job seekers. The first thing a new account does is upload the resume it already has. Last month 21 people signed up, 9 saved a resume, and none came back a second day. My usage table showed no failed call in two weeks, so I assumed uploads were fine. They were not.
The symptom
I made a throwaway account and uploaded a small text PDF. The toast said "We couldn't read that file". I uploaded the same file again and it worked. Same bytes, different answer.
Reproducing it outside the app
The upload route uses pdf-parse, which bundles pdf.js v1.10.100 from 2018. I took the route out of the picture and called the library 80 times in a row on the same 2.4 KB file, printing X for a throw and . for a result:
XX..............................................................................
The first two calls throw bad XRef entry. Every call after that succeeds. Six fresh processes gave the same picture, whether the first call came 0 ms or 1.5 s after the module loaded, so it is a count, not a timer. The file is not broken: it was written by ReportLab, and its startxref points exactly at the xref table. A PDF produced by my own templates never failed.
On a laptop that is a curiosity. On serverless it is the normal case: a cold function is a fresh process, so a person whose upload lands on one gets the failure.
Why nothing showed it
Simplified, the route did this:
try {
text = await extractText(buffer, file.type);
} catch (e) {
return json({ error: "We couldn't read that file." }, 400);
}
const parsed = await parseResumeText(text, user.id); // the first logged call
Only model calls were written to the usage table, and the failure happens one line before the first of them. "Zero errors" was true and meant nothing. Three of those 21 accounts had a resume parsed and never got one saved, and I cannot tell how many never got that far.
What I changed
- Retry the local parser. In a plain Node process the third call works, so four tries cover it.
- When it still fails, hand the file itself to a model that reads PDFs and images and ask for a plain transcription. That also covers scans and photos of a resume, which have no text layer at all and had always ended in the same error.
- Log every such read with its reason (
pdf_unreadable: bad XRef entry,pdf_no_text_layer,image), success or not. - Detect the type from the first bytes. Phones hand over PDFs as
application/octet-streamor with no type, and those were being rejected as unsupported.
Then I checked production, and step 1 turned out to be worth less than I thought. On the deployed function the same file failed all four tries and was read by the fallback in two seconds. The retry that fixes a local process does not fix a serverless one, and I only know that because step 3 wrote a row saying so. Eight fallback reads cost about a third of a cent in total by my own price table.
One place does not get the fallback: the ATS checker. Its job is to report what a parser finds in the file, and for a scan the true answer is "nothing". Transcribing the picture there would turn that finding into a pass.
What I took from it
- A dashboard that only counts the calls you log shows zero errors for everything that fails before them. Log the step in front of the first "real" call.
- Test a parser in a fresh process, not in a warm REPL or a test runner that has already called it.
- A fix measured locally is a guess until production writes down what it did.
The site is ResumeBanao, a resume builder for India; the same upload sits behind the free ATS checker. Written by an AI assistant from my own build notes and usage logs.
Originally published by Dev.to WebDev. Aggregated on AIWithGhost for educational purposes — full credit and traffic to the original publisher.