we-promise/sure · error · Provider::Openai::Error

Could not extract text from PDF

Error message

Could not extract text from PDF

What it means

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.

Source

Thrown at app/models/provider/openai/bank_statement_extractor.rb:13

class Provider::Openai::BankStatementExtractor
  MAX_CHARS_PER_CHUNK = 3000
  attr_reader :client, :pdf_content, :model

  def initialize(client:, pdf_content:, model:)
    @client = client
    @pdf_content = pdf_content
    @model = model
  end

  def extract
    pages = extract_pages_from_pdf
    raise Provider::Openai::Error, "Could not extract text from PDF" if pages.empty?

    chunks = build_chunks(pages)
    Rails.logger.info("BankStatementExtractor: Processing #{chunks.size} chunk(s) from #{pages.size} page(s)")

    all_transactions = []
    metadata = {}

    chunks.each_with_index do |chunk, index|
      Rails.logger.info("BankStatementExtractor: Processing chunk #{index + 1}/#{chunks.size}")
      result = process_chunk(chunk, index == 0)

      # Tag transactions with chunk index for deduplication
      tagged_transactions = (result[:transactions] || []).map { |t| t.merge(chunk_index: index) }
      all_transactions.concat(tagged_transactions)

      if index == 0
        metadata = {
          account_holder: result[:account_holder],

View on GitHub (pinned to e69894adb9)

Solutions

  1. Check the log for the swallowed exception ('Failed to extract text from PDF: ...') — encrypted/malformed PDFs identify themselves there.
  2. If the PDF is scanned, use a vision-based path (Provider::Openai::PdfProcessor#process_with_vision converts pages to images) instead of text extraction.
  3. Validate the upload before processing: non-empty bytes, %PDF- magic header, and page.text present on at least one page.
  4. For encrypted statements, decrypt with the user's password via a PDF library before extraction.

Example fix

# before
pages = extract_pages_from_pdf
raise Provider::Openai::Error, "Could not extract text from PDF" if pages.empty?

# after
pages = extract_pages_from_pdf
if pages.empty?
  raise Provider::Openai::Error, "Could not extract text from PDF (likely scanned or encrypted)" unless vision_capable?
  extract_with_vision # fall back to page-image processing
end
Defensive patterns

Strategy: validation

Validate before calling

raise ArgumentError, "upload is empty" if pdf_content.to_s.empty?
raise ArgumentError, "not a PDF" unless pdf_content.start_with?("%PDF-")

Type guard

def text_extractable_pdf?(bytes)
  return false if bytes.blank?
  reader = PDF::Reader.new(StringIO.new(bytes))
  reader.pages.any? { |p| p.text.to_s.strip.present? }
rescue StandardError
  false
end

Try / catch

begin
  extractor.extract
rescue Provider::Openai::Error => e
  raise unless e.message.include?("Could not extract text")
  render_error(:scanned_or_encrypted_pdf)
end

Prevention

When it happens

Trigger: 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.

Common situations: 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.

Related errors


AI-assisted analysis of we-promise/sure@e69894adb9 (2026-08-21). Data as JSON: /api/errors/282fffa25770e20d. Report an issue: GitHub.