{"record":{"id":"282fffa25770e20d","repo":"we-promise/sure","slug":"could-not-extract-text-from-pdf","errorCode":null,"errorMessage":"Could not extract text from PDF","messagePattern":"Could not extract text from PDF","errorType":"exception","errorClass":"Provider::Openai::Error","httpStatus":null,"severity":"error","filePath":"app/models/provider/openai/bank_statement_extractor.rb","lineNumber":13,"sourceCode":"class Provider::Openai::BankStatementExtractor\n  MAX_CHARS_PER_CHUNK = 3000\n  attr_reader :client, :pdf_content, :model\n\n  def initialize(client:, pdf_content:, model:)\n    @client = client\n    @pdf_content = pdf_content\n    @model = model\n  end\n\n  def extract\n    pages = extract_pages_from_pdf\n    raise Provider::Openai::Error, \"Could not extract text from PDF\" if pages.empty?\n\n    chunks = build_chunks(pages)\n    Rails.logger.info(\"BankStatementExtractor: Processing #{chunks.size} chunk(s) from #{pages.size} page(s)\")\n\n    all_transactions = []\n    metadata = {}\n\n    chunks.each_with_index do |chunk, index|\n      Rails.logger.info(\"BankStatementExtractor: Processing chunk #{index + 1}/#{chunks.size}\")\n      result = process_chunk(chunk, index == 0)\n\n      # Tag transactions with chunk index for deduplication\n      tagged_transactions = (result[:transactions] || []).map { |t| t.merge(chunk_index: index) }\n      all_transactions.concat(tagged_transactions)\n\n      if index == 0\n        metadata = {\n          account_holder: result[:account_holder],","sourceCodeStart":1,"sourceCodeEnd":31,"githubUrl":"https://github.com/we-promise/sure/blob/e69894adb92547273377398c15f45c979cd9416a/app/models/provider/openai/bank_statement_extractor.rb#L1-L31","documentation":"Raised by Provider::Openai::BankStatementExtractor#extract when extract_pages_from_pdf yields zero pages. That helper returns [] when pdf_content is blank, when PDF::Reader raises (malformed/encrypted PDF — the rescue swallows the exception into a log line 'Failed to extract text from PDF'), or when every page's text is blank after reject(&:blank?). The dominant real-world cause is a scanned/image-only statement with no text layer, which pdf-reader cannot read.","triggerScenarios":"Uploading a scanned bank statement (photos of pages, no OCR layer); a password-protected/encrypted PDF that PDF::Reader cannot open; a corrupt or zero-byte upload; a PDF whose pages contain only images; pdf_content passed as nil/empty from an upstream download failure.","commonSituations":"Bank exports that are image-based scans; users re-saving statements through scanner apps; mobile uploads of photographed statements; test fixtures that are placeholder bytes; encrypted e-statement PDFs (many Indian banks) whose password was never supplied.","solutions":["Check the log for the swallowed exception ('Failed to extract text from PDF: ...') — encrypted/malformed PDFs identify themselves there.","If the PDF is scanned, use a vision-based path (Provider::Openai::PdfProcessor#process_with_vision converts pages to images) instead of text extraction.","Validate the upload before processing: non-empty bytes, %PDF- magic header, and page.text present on at least one page.","For encrypted statements, decrypt with the user's password via a PDF library before extraction."],"exampleFix":"# before\npages = extract_pages_from_pdf\nraise Provider::Openai::Error, \"Could not extract text from PDF\" if pages.empty?\n\n# after\npages = extract_pages_from_pdf\nif pages.empty?\n  raise Provider::Openai::Error, \"Could not extract text from PDF (likely scanned or encrypted)\" unless vision_capable?\n  extract_with_vision # fall back to page-image processing\nend","handlingStrategy":"validation","validationCode":"raise ArgumentError, \"upload is empty\" if pdf_content.to_s.empty?\nraise ArgumentError, \"not a PDF\" unless pdf_content.start_with?(\"%PDF-\")","typeGuard":"def text_extractable_pdf?(bytes)\n  return false if bytes.blank?\n  reader = PDF::Reader.new(StringIO.new(bytes))\n  reader.pages.any? { |p| p.text.to_s.strip.present? }\nrescue StandardError\n  false\nend","tryCatchPattern":"begin\n  extractor.extract\nrescue Provider::Openai::Error => e\n  raise unless e.message.include?(\"Could not extract text\")\n  render_error(:scanned_or_encrypted_pdf)\nend","preventionTips":["Pre-check that at least one page has a text layer; route image-only PDFs to the vision path.","Reject encrypted uploads early and prompt for the password instead of failing deep in extraction.","Check the log line 'Failed to extract text from PDF' — it carries the real PDF::Reader exception."],"tags":["openai","pdf","pdf-reader","text-extraction","scanned-pdf"],"backgroundTag":"pdf-text-extraction-failed","analyzedSha":"e69894adb92547273377398c15f45c979cd9416a","analyzedAt":"2026-08-21T18:22:41.165Z","schemaVersion":2},"datasetVersion":"2026-08-21T23:17:16.201Z"}