From ed933080367d1df6c3a3e57f0d60e828e966a07b Mon Sep 17 00:00:00 2001 From: Thorsten Sommer Date: Sun, 9 Aug 2026 21:10:15 +0200 Subject: [PATCH] Fixed a single unreadable PDF page silently truncating the document --- runtime/src/file_data.rs | 58 +++++++++++++++++++++++++++++++++++++--- 1 file changed, 54 insertions(+), 4 deletions(-) diff --git a/runtime/src/file_data.rs b/runtime/src/file_data.rs index 7aec075b..a7eb2c84 100644 --- a/runtime/src/file_data.rs +++ b/runtime/src/file_data.rs @@ -312,7 +312,15 @@ async fn stream_data(file_path: &str, extract_images: bool) -> Result format!("{} bytes", metadata.len()), + Err(error) => format!("unknown size ({error})"), + }; + + debug!("Extracting data from file: '{file_path}', {file_size}, format: '{fmt:?}', extension: '{ext}'"); let stream = match ext.as_str() { // @@ -509,27 +517,69 @@ async fn stream_pdf(file_path: &str) -> Result { } }; + let mut number_of_pages = 0; + let mut number_of_characters = 0; + let mut number_of_failed_pages = 0; + let mut receiver_gone = false; + for (num_page, page) in doc.pages().iter().enumerate() { let page_number = num_page + 1; + number_of_pages = page_number; + let content = match page.text().map(|t| t.all()) { Ok(text_content) => text_content, Err(e) => { - let _ = tx.blocking_send(Err(ExtractionError::on_page( + // + // A single unreadable page must not end the document: we report it as a + // non-fatal error chunk and continue with the next page. Sending it as an + // `Err` would stop the consumer and silently truncate everything after it. + // + number_of_failed_pages += 1; + warn!("The text of page {page_number} of '{path}' could not be extracted: {e}"); + + if tx.blocking_send(Ok(Chunk::from_error(&ExtractionError::on_page( ExtractionErrorCode::PageExtractionFailed, format!("The text of page {page_number} could not be extracted: {e}"), page_number, - ).into())); + )))).is_err() { + receiver_gone = true; + break; + } + continue; } }; + number_of_characters += content.chars().count(); + if tx.blocking_send(Ok(Chunk::new( - content, + content, Metadata::Pdf { page_number } ))).is_err() { + receiver_gone = true; break; } } + + if receiver_gone { + debug!("The consumer stopped reading the PDF stream of '{path}' after {number_of_pages} page(s)."); + return; + } + + debug!("Extracted {number_of_characters} character(s) from {number_of_pages} page(s) of '{path}'; failed pages: {number_of_failed_pages}."); + + // + // Without this marker, a PDF without a text layer and a broken extraction both arrive as + // an empty document, and the AI would answer as if the file had no content at all. + // + if number_of_characters == 0 { + warn!("No text could be extracted from '{path}': {number_of_pages} page(s), {number_of_failed_pages} failed page(s). The PDF may consist of scanned images without a text layer."); + + let _ = tx.blocking_send(Ok(Chunk::from_error(&ExtractionError::new( + ExtractionErrorCode::NoTextExtracted, + format!("No text could be extracted from {number_of_pages} page(s). The PDF may consist of scanned images without a text layer."), + )))); + } }); Ok(Box::pin(ReceiverStream::new(rx)))