Stirling-Tools/Stirling-PDF · error · Error

Response is not a valid PDF. Header: "${head}"

Error message

Response is not a valid PDF. Header: "${head}"

What it means

Thrown by the OCR response handler when the blob's first 8 bytes are neither a PDF magic ('%PDF') nor a ZIP magic ('PK'), and the first 1KB does not look like an error/exception/HTML body. It is the 'unrecognized response' fallback — the server returned something, but it is neither a valid OCR output nor a recognizable error.

Source

Thrown at frontend/editor/src/core/hooks/tools/ocr/useOCROperation.ts:147

  }

  // Not a PDF: surface error details if present
  if (!head.startsWith("%PDF")) {
    const textBuf = await blob.slice(0, 1024).arrayBuffer();
    const text = new TextDecoder().decode(new Uint8Array(textBuf));
    if (/error|exception|html/i.test(text)) {
      if (text.includes("OCR tools") && text.includes("not installed")) {
        throw new Error(
          "OCR tools (OCRmyPDF or Tesseract) are not installed on the server. Use the standard or fat Docker image instead of ultra-lite, or install OCR tools manually.",
        );
      }
      const title =
        text.match(/<title[^>]*>([^<]+)<\/title>/i)?.[1] ||
        text.match(/<h1[^>]*>([^<]+)<\/h1>/i)?.[1] ||
        "Unknown error";
      throw new Error(`OCR service error: ${title}`);
    }
    throw new Error(`Response is not a valid PDF. Header: "${head}"`);
  }

  const originalName = originalFiles[0].name;
  return [new File([blob], originalName, { type: "application/pdf" })];
};

// Static configuration object (without t function dependencies)
export const ocrOperationConfig = defineSingleFileTool({
  validateParams: validateOCRParameters,
  buildFormData: buildOCRFormData,
  toApiParams: ocrToApiParams,
  fromApiParams: ocrFromApiParams,
  operationType: "ocr",
  endpoint: ENDPOINT,
  defaultParameters,
});

export const useOCROperation = () => {

View on GitHub (pinned to 9ef20dcab8)

Solutions

  1. Log the full Content-Type, Content-Length, and first bytes for the failing response to classify it.
  2. Check backend logs — a 200 with empty body usually means the controller returned before writing output (early return / swallowed exception).
  3. Verify network/proxy is not truncating the body (compare Content-Length to actual bytes).
  4. Treat HTTP non-2xx explicitly before parsing the body so error responses are not misclassified as 'not a PDF'.

Example fix

// before
throw new Error(`Response is not a valid PDF. Header: "${head}"`);

// after — include size + content-type for diagnosis
throw new Error(`Response is not a valid PDF. Header: "${head}" (size=${blob.size}, type=${blob.type})`);
Defensive patterns

Strategy: validation

Validate before calling

// Reject non-PDF responses before parsing
if (!head.startsWith("%PDF") && !head.startsWith("PK")) {
  const ct = response.headers?.["content-type"];
  if (!ct?.includes("pdf") && !ct?.includes("zip")) {
    // classify as protocol error with status + size, do not attempt PDF parsing
  }
}

Try / catch

try {
  await runOcr();
} catch (e) {
  if (e instanceof Error && e.message.startsWith("Response is not a valid PDF.")) {
    // log size/type and check backend for empty-body bug
    console.error("Unrecognized OCR response", { size: blob.size, type: blob.type });
    toast.error("OCR returned an unexpected response.");
  } else throw e;
}

Prevention

When it happens

Trigger: Server returned a 200 with an empty body or whitespace; response is a binary fragment of an unknown format; a CDN/cache layer returned a truncated response; content-encoding/transfer corruption truncated the magic bytes; the OCR endpoint returned an octet-stream that is not actually a PDF.

Common situations: Empty 200 from a misconfigured endpoint; truncated response on a flaky connection; the backend changed its output format without the frontend handler being updated.

Related errors


AI-assisted analysis of Stirling-Tools/Stirling-PDF@9ef20dcab8 (2026-08-13). Data as JSON: /api/errors/cf448e81d94372df. Report an issue: GitHub.