Stirling-Tools/Stirling-PDF · error · Error

OCR service error: ${title}

Error message

OCR service error: ${title}

What it means

Thrown by the OCR response handler when the response is not a PDF/ZIP but its first 1KB matches /error|exception|html/ and does not match the OCR-tools-not-installed substring. It parses an HTML <title> or <h1> and reports it as the service error. This is a catch-all for server-side errors that came back as HTML (Spring error page) or plain text instead of a valid PDF.

Source

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

    }
    return [new File([blob], `ocr_${base}.zip`, { type: "application/zip" })];
  }

  // 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,
});

View on GitHub (pinned to 9ef20dcab8)

Solutions

  1. Check backend logs around the time of the request for the matching exception/stack trace.
  2. Verify the request payload (file size, language param) is within the endpoint's limits; OCRmyPDF can fail on very large or corrupt PDFs.
  3. If the title is a proxy/gateway error (502/504), raise upstream timeouts for the OCR endpoint.
  4. Add server-side structured error responses (JSON) so the frontend can route by status code instead of scraping HTML.

Example fix

// before
const title = text.match(/<title[^>]*>([^<]+)<\/title>/i)?.[1] || text.match(/<h1[^>]*>([^<]+)<\/h1>/i)?.[1] || "Unknown error";
throw new Error(`OCR service error: ${title}`);

// after — include HTTP status context for diagnosis
throw new Error(`OCR service error: ${title} (HTTP ${response.status} from ${ENDPOINT})`);
Defensive patterns

Strategy: try-catch

Validate before calling

// Treat non-2xx explicitly before parsing the body
if (response.status < 200 || response.status >= 300) {
  // surface `OCR service error: HTTP ${response.status}` without scraping HTML
}

Try / catch

try {
  await runOcr();
} catch (e) {
  if (e instanceof Error && e.message.startsWith("OCR service error:")) {
    toast.error(e.message);
    // surface backend logs hint to ops
  } else throw e;
}

Prevention

When it happens

Trigger: Backend threw an exception (500) and Spring rendered its default error/whitelabel page; a reverse proxy (nginx, gateway) returned its own HTML error page; the OCR endpoint hit a size/time limit and returned an HTML error; a malformed request caused a 4xx with an HTML body.

Common situations: Transient backend crash during OCR; reverse-proxy timeout returning HTML; the OCRmyPDF subprocess crashed and the controller forwarded the stack trace as HTML.

Related errors


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