Skip to content

Commit 18a1e2f

Browse files
authored
fix: catch NotFoundError in artifact fetchers to restore get_job_run_error output (#742)
## Why In v1.15, two changes combined to break `get_job_run_error`: 1. `get_job_run_artifact` was changed to raise `NotFoundError` for 404 responses instead of `ArtifactRetrievalError` (#729) 2. `JobRunFetcher._fetch_run_results_artifact` and `_fetch_sources_artifact` only caught `ArtifactRetrievalError` When a step's artifact returns 404 (normal for many steps), the uncaught `NotFoundError` propagated into `asyncio.gather(..., return_exceptions=True)`, which treated each step as a failed exception and skipped it — resulting in `{"failed_steps": []}` even for genuinely failed runs. Fixes #740 ## What - Catch `(ArtifactRetrievalError, NotFoundError)` in both `_fetch_run_results_artifact` and `_fetch_sources_artifact` so 404s fall back to log-based error output as intended - Add regression test that verifies `NotFoundError` from `get_job_run_artifact` is handled gracefully ## Checklist - [x] I have performed a self-review of my code - [x] I have made corresponding changes to the documentation if required - [x] I have added tests that prove my fix is effective or that my feature works - [x] New and existing unit tests pass locally with my changes
1 parent 83e7b0a commit 18a1e2f

3 files changed

Lines changed: 52 additions & 4 deletions

File tree

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
kind: Bug Fix
2+
body: Fix get_job_run_error returning empty failed_steps when artifact 404s raise NotFoundError
3+
time: 2026-04-27T15:09:13.154529+02:00

src/dbt_mcp/dbt_admin/run_artifacts/parser.py

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -21,7 +21,7 @@
2121
RunStepSchema,
2222
SourcesArtifactSchema,
2323
)
24-
from dbt_mcp.errors import ArtifactRetrievalError
24+
from dbt_mcp.errors import ArtifactRetrievalError, NotFoundError
2525
from pydantic import ValidationError
2626

2727
logger = logging.getLogger(__name__)
@@ -88,7 +88,7 @@ async def _fetch_run_results_artifact(self, step: RunStepSchema) -> str | None:
8888
logger.info(f"Got run_results.json from step {step_index}")
8989
return run_results_content
9090
return None
91-
except ArtifactRetrievalError as e:
91+
except (ArtifactRetrievalError, NotFoundError) as e:
9292
logger.debug(f"No run_results.json for step {step_index}: {e}")
9393
return None
9494

@@ -107,7 +107,7 @@ async def _fetch_sources_artifact(self, step: RunStepSchema) -> str | None:
107107
logger.info(f"Got sources.json from step {step_index}")
108108
return sources_content
109109
return None
110-
except ArtifactRetrievalError as e:
110+
except (ArtifactRetrievalError, NotFoundError) as e:
111111
logger.debug(f"No sources.json for step {step_index}: {e}")
112112
return None
113113

tests/unit/dbt_admin/run_artifacts/test_error_fetcher.py

Lines changed: 46 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@
44
import pytest
55

66
from dbt_mcp.dbt_admin.run_artifacts.parser import ErrorFetcher
7-
from dbt_mcp.errors import ArtifactRetrievalError
7+
from dbt_mcp.errors import ArtifactRetrievalError, NotFoundError
88

99

1010
@pytest.mark.parametrize(
@@ -195,3 +195,48 @@ async def test_schema_validation_failure(mock_client, admin_config):
195195
assert step["step_name"] == "Invoke dbt with `dbt build`"
196196
assert "run_results.json not available" in step["results"][0]["message"]
197197
assert "Model compilation failed" in step["results"][0]["truncated_logs"]
198+
199+
200+
async def test_not_found_error_treated_as_missing_artifact(mock_client, admin_config):
201+
"""Regression test: NotFoundError (404) from get_job_run_artifact must be caught.
202+
203+
In v1.15, get_job_run_artifact started raising NotFoundError for 404 responses
204+
instead of ArtifactRetrievalError. If _fetch_run_results_artifact only catches
205+
ArtifactRetrievalError, the NotFoundError propagates into asyncio.gather and
206+
causes every failed step to be silently dropped, returning {"failed_steps": []}.
207+
"""
208+
run_details = {
209+
"id": 500,
210+
"status": 20,
211+
"is_cancelled": False,
212+
"finished_at": "2024-01-01T12:00:00Z",
213+
"run_steps": [
214+
{
215+
"index": 1,
216+
"name": "Invoke dbt with `dbt run`",
217+
"status": 20,
218+
"finished_at": "2024-01-01T12:00:00Z",
219+
"logs": "Error in model my_model",
220+
}
221+
],
222+
}
223+
224+
mock_client.get_job_run_artifact = AsyncMock(
225+
side_effect=NotFoundError("Artifact not found for run 500")
226+
)
227+
228+
error_fetcher = ErrorFetcher(
229+
run_id=500,
230+
run_details=run_details,
231+
client=mock_client,
232+
admin_api_config=admin_config,
233+
)
234+
235+
result = await error_fetcher.analyze_run_errors()
236+
237+
# Should fall back to logs rather than returning empty failed_steps
238+
assert len(result["failed_steps"]) == 1
239+
step = result["failed_steps"][0]
240+
assert step["step_name"] == "Invoke dbt with `dbt run`"
241+
assert "run_results.json not available" in step["results"][0]["message"]
242+
assert "Error in model my_model" in step["results"][0]["truncated_logs"]

0 commit comments

Comments
 (0)