amruthpillai/reactive-resume · error · ORPCError
BAD_REQUEST
BAD_REQUEST
Error message
Invalid public PDF fallback metadata.
What it means
createPublicResumePdf() validates the semantic-CSS fallback metadata before doing any work: mismatchReason must be one of the eight PUBLIC_RESUME_PDF_MISMATCH_REASONS, and any provided clientRegistryFingerprint/clientAdapterFingerprint must match /^[a-f0-9]{64}$/ (lowercase SHA-256 hex). This is the trust boundary for the public fallback render path.
Source
Thrown at packages/api/src/features/resume/public-pdf.ts:71
rateLimiter: publicRenderRateLimiter,
renderPdf: async (input) => (await import("@reactive-resume/pdf/server")).createResumePdfFile(input),
getFingerprints: async () =>
(await import("@reactive-resume/pdf/public-projection")).getPublicStyleProjectionFingerprints(),
now: Date.now,
observe: console.info,
};
export async function createPublicResumePdf(
input: CreatePublicResumePdfInput,
dependencies: PublicResumePdfDependencies = defaultDependencies,
): Promise<{ body: File; filename: string }> {
const fingerprintPattern = /^[a-f0-9]{64}$/;
if (
!PUBLIC_RESUME_PDF_MISMATCH_REASONS.includes(input.mismatchReason) ||
(input.clientRegistryFingerprint !== undefined && !fingerprintPattern.test(input.clientRegistryFingerprint)) ||
(input.clientAdapterFingerprint !== undefined && !fingerprintPattern.test(input.clientAdapterFingerprint))
) {
throw new ORPCError("BAD_REQUEST", { status: 400, message: "Invalid public PDF fallback metadata." });
}
const currentUserId = await dependencies.resolveCurrentUserId(input.requestHeaders);
const resume = await loadAuthorizedPublicRenderResume(
{
username: input.username,
slug: input.slug,
requestHeaders: input.requestHeaders,
trustedClient: input.trustedClient,
...(currentUserId ? { currentUserId } : {}),
},
dependencies,
);
dependencies.rateLimiter.consume({ trustedClient: input.trustedClient, resumeId: resume.id });
const startedAt = dependencies.now();
const fingerprints = await dependencies.getFingerprints();
const event = (success: boolean) => {
dependencies.observe({
name: "semantic_css.render_fallback",View on GitHub (pinned to 3a5b12e2a4)
Solutions
- Send only mismatchReason values from PUBLIC_RESUME_PDF_MISMATCH_REASONS.
- Compute fingerprints as lowercase hex SHA-256 (exactly 64 chars).
- Omit fingerprints when unknown rather than sending malformed values.
Example fix
// before
createPublicResumePdf({ ..., mismatchReason: 'unknown', clientRegistryFingerprint: 'ABC123' });
// after
const REASONS = ['missing-projection','format-version','language-version','semantic-tree-version','registry-fingerprint','adapter-fingerprint','render-data-hash','invalid-projection'] as const;
const fp = crypto.createHash('sha256').update(payload).digest('hex'); // 64 lowercase hex
createPublicResumePdf({ ..., mismatchReason: REASONS[i], clientRegistryFingerprint: fp }); Defensive patterns
Strategy: validation
Validate before calling
const REASONS = ['missing-projection','format-version','language-version','semantic-tree-version','registry-fingerprint','adapter-fingerprint','render-data-hash','invalid-projection'] as const;
const FP = /^[a-f0-9]{64}$/;
function validateFallbackMeta(reason, registryFp, adapterFp) {
if (!REASONS.includes(reason)) throw new Error(`Bad mismatchReason: ${reason}`);
if (registryFp !== undefined && !FP.test(registryFp)) throw new Error('Bad registry fingerprint');
if (adapterFp !== undefined && !FP.test(adapterFp)) throw new Error('Bad adapter fingerprint');
} Type guard
function isMismatchReason(value: unknown): value is PublicResumePdfMismatchReason {
return typeof value === 'string' && PUBLIC_RESUME_PDF_MISMATCH_REASONS.includes(value as any);
} Prevention
- Keep the client's mismatch-reason vocabulary in sync with PUBLIC_RESUME_PDF_MISMATCH_REASONS.
- Compute fingerprints as lowercase hex SHA-256 (64 chars), never base64 or uppercase.
- Omit fingerprints when unsure rather than sending malformed values.
When it happens
Trigger: Calling the public resume PDF fallback with an unknown mismatchReason, a fingerprint of the wrong length, or non-hex characters (uppercase, a 'sha256:' prefix, base64, etc.).
Common situations: A client using an outdated reason vocabulary after a schema bump, a fingerprint computed with a different hash algorithm or encoding, or a malformed probe request.
Related errors
AI-assisted analysis of amruthpillai/reactive-resume@3a5b12e2a4 (2026-08-12).
Data as JSON: /api/errors/3c07acd980d3ad89.
Report an issue: GitHub.