From 3c7dbf0966e253130511f8e78b13cde0eb9f9c66 Mon Sep 17 00:00:00 2001 From: Chris McDonough Date: Mon, 1 Jun 2026 07:54:08 -0400 Subject: [PATCH] Classify PermissionError as PermanentError instead of TransientError MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit PermissionError is a subclass of OSError, so it was caught by the broad timeout/io handler and classified as transient. An unreadable file would retry 5 times then DLQ — permissions don't fix themselves without operator intervention. Add an explicit PermissionError check before the OSError catch so unreadable files go straight to the DLQ. --- haiku_rag_slim/haiku/rag/ingester/workers/pipeline.py | 3 +++ tests/ingester/test_pipeline.py | 9 +++++++++ 2 files changed, 12 insertions(+) diff --git a/haiku_rag_slim/haiku/rag/ingester/workers/pipeline.py b/haiku_rag_slim/haiku/rag/ingester/workers/pipeline.py index d626adf9..20f018ab 100644 --- a/haiku_rag_slim/haiku/rag/ingester/workers/pipeline.py +++ b/haiku_rag_slim/haiku/rag/ingester/workers/pipeline.py @@ -60,6 +60,9 @@ def _classify(exc: BaseException) -> Exception: if isinstance(exc, FileNotFoundError): return PermanentError(f"file not found: {exc}") + if isinstance(exc, PermissionError): + return PermanentError(f"permission denied: {exc}") + if isinstance(exc, asyncio.TimeoutError | TimeoutError | OSError): return TransientError(f"timeout/io: {exc}") diff --git a/tests/ingester/test_pipeline.py b/tests/ingester/test_pipeline.py index 64e523f8..218a9d26 100644 --- a/tests/ingester/test_pipeline.py +++ b/tests/ingester/test_pipeline.py @@ -270,3 +270,12 @@ async def test_file_not_found_classified_as_permanent(): client.create_document_from_source.side_effect = FileNotFoundError("gone") with pytest.raises(PermanentError, match="file not found"): await run_job(client, _job()) + + +@pytest.mark.asyncio +async def test_permission_error_classified_as_permanent(): + """An unreadable file should go straight to the DLQ, not retry.""" + client = _mock_client() + client.create_document_from_source.side_effect = PermissionError("no access") + with pytest.raises(PermanentError, match="permission denied"): + await run_job(client, _job())