Resuzen builds a résumé as LaTeX and hands back a PDF. It runs on serverless functions, and there is no TeX install on them. Compiling happens somewhere else: the function posts the source to a hosted LaTeX compiler and waits for a PDF to come back.
That makes the core of the product a network call to a service I don't run. The compile module is small, and most of it exists to handle the ways that call goes wrong.
Two providers, in order
const PROVIDERS = [
{ name: 'ytotech', compile: compileYtotech },
{ name: 'texlive', compile: compileTexlive },
];The first is tried first. If it is unreachable, times out or answers with a server error, the second gets the same source. Each provider gets one retry after a short pause before the loop moves on.
A budget, not just a timeout
A timeout per request is not enough when there can be four requests. Two providers with a retry each, at twelve seconds apiece, is longer than the function is allowed to run. A function that is killed for running too long doesn't get to return a useful error.
So there are two limits:
const DEFAULT_TIMEOUT_MS = 12000; // one attempt
const DEFAULT_BUDGET_MS = 25000; // everythingThe route is allowed 30 seconds. The compile gives itself 25 in total, and every attempt is capped at whichever is smaller, the per-attempt timeout or the time that is left:
return await attempt(provider, tex, Math.min(timeoutMs, remaining));When the budget runs out the loop stops and the function returns its own error, on purpose, while it still can.
Not every failure deserves a retry
There are two kinds of failure and they need opposite handling.
If the provider is down or slow, trying again or trying the other one is the right move. If the LaTeX itself is broken, every provider will reject it the same way, and four attempts just make the person wait longer to hear the same answer.
The module keeps them apart with two error types. A source error is not retried on the same provider. It is tried once on the next one, in case the two differ in which packages they have, and then it is reported. If any provider said the source was bad, that is the error the person sees, because it is the one they can act on.
The five bytes that mattered
The two providers report a failed compile differently. One answers with a 4xx status. The other answers 200 OK and sends the TeX log as plain text where the PDF should be.
A status check passes that second response straight through. The function would hand back a TeX log as someone's résumé.
So the function checks what it actually received. Every PDF starts with the same five bytes, %PDF-:
function isPdf(buf) {
if (!buf || buf.byteLength < 5) return false;
const b = new Uint8Array(buf, 0, 5);
return b[0] === 0x25 && b[1] === 0x50 && b[2] === 0x44 && b[3] === 0x46 && b[4] === 0x2d;
}Anything else is a failed compile, whatever the status code said.
That function is short and I still got it wrong twice. The first version read five bytes and compared four, so a body starting %PDFx passed. It also had no length check, and new Uint8Array(buf, 0, 5) throws a RangeError when the body is shorter than five bytes. An empty 200 didn't fail the check. It crashed it. Both are covered by tests now.
The log is someone's résumé
One more thing came out of handling failures. A TeX error log quotes the line that failed, and the line that failed is the user's own text. Logging the provider's response on error meant writing pieces of people's résumés into server logs.
Now the code looks for the first error in the log and records only a name from a fixed list, like undefined-control-sequence at line 42. That is enough to find the bug in a template, and it contains no words from the document.
What I'd take from it
- Give the whole operation a deadline shorter than the platform's, so you fail in your own code.
- Separate "the service is unhealthy" from "the input is bad" before you write the retry loop.
- Check that the thing you got back is the thing you asked for. A status code is the server's opinion.
The rest of Resuzen is written up in the case study.