datawhalechina/hello-agents · warning · HTTPException
report artifact not found
Error message
report artifact not found
What it means
GET /api/outputs/{filename} serves generated report artifacts from OUTPUTS_DIR and raises HTTPException(404, 'report artifact not found') when the path does not exist or is not a regular file. The artifact is created by the chat/report-generation flow, which returns a download_url built with url_for — so a 404 here means generation never actually wrote the file (or wrote it after the URL was handed out), or the request used a wrong/manually constructed filename.
Source
Thrown at Co-creation-projects/monkeyhlj-NetworkHealthReportAgent/src/api/main.py:99
@app.get("/api/reports")
def get_all_site_reports(
start_date: str | None = Query(default=None, description="YYYY-MM-DD"),
end_date: str | None = Query(default=None, description="YYYY-MM-DD"),
) -> dict:
start, end = default_date_window(start_date=start_date, end_date=end_date, days=7)
reports = []
for site in orchestrator.list_sites():
reports.append(orchestrator.build_report(site_id=site["site_id"], start=start, end=end))
return {"count": len(reports), "reports": reports}
@app.get("/api/outputs/{filename}", name="download_generated_report")
def download_generated_report(filename: str) -> FileResponse:
file_path = OUTPUTS_DIR / filename
if not file_path.exists() or not file_path.is_file():
raise HTTPException(status_code=404, detail="report artifact not found")
return FileResponse(file_path, filename=filename, media_type="text/markdown; charset=utf-8")
@app.post("/api/chat")
def ask_global_question(payload: AskRequest, request: Request) -> dict:
start, end = default_date_window(
start_date=payload.start_date,
end_date=payload.end_date,
days=7,
)
try:
answer = orchestrator.ask_global_question(
question=payload.question,
start=start,
end=end,
site_id=payload.site_id,
)
artifact = answer.get("artifact")View on GitHub (pinned to 606a07d341)
Solutions
- Verify the file exists server-side: ls $OUTPUTS_DIR and compare with the filename in the download_url.
- Retry the download once generation verifiably completed (poll file existence or wait for the response that includes the artifact before enabling the link).
- Make OUTPUTS_DIR absolute/configurable so restarts from other cwds do not orphan old artifacts.
- Ensure the generation code writes the file (and flushes/fsyncs) before attaching download_url to the response.
- Reject path-traversal filenames explicitly (see defense) — exists()/is_file() alone also admits ../ escapes on some setups.
Example fix
# before
@app.get("/api/outputs/{filename}")
def download_generated_report(filename: str) -> FileResponse:
file_path = OUTPUTS_DIR / filename
# after
from pathlib import Path
@app.get("/api/outputs/{filename}")
def download_generated_report(filename: str) -> FileResponse:
if "/" in filename or "\\" in filename or ".." in filename:
raise HTTPException(status_code=400, detail="invalid filename")
file_path = (OUTPUTS_DIR / filename).resolve()
if OUTPUTS_DIR.resolve() not in file_path.parents:
raise HTTPException(status_code=400, detail="invalid filename")
if not file_path.is_file():
raise HTTPException(status_code=404, detail="report artifact not found") Defensive patterns
Strategy: validation
Validate before calling
from pathlib import Path
def safe_artifact_path(outputs_dir: Path, filename: str) -> Path | None:
if not filename or "/" in filename or "\\" in filename or ".." in filename:
return None
p = (outputs_dir / filename).resolve()
return p if p.parent == outputs_dir.resolve() and p.is_file() else None Try / catch
resp = requests.get(download_url)
if resp.status_code == 404:
# artifact not yet written — retry once after generation completes
time.sleep(1)
resp = requests.get(download_url)
resp.raise_for_status() Prevention
- Write and flush artifacts before returning download_url
- Use absolute OUTPUTS_DIR anchored to config, not cwd
- Reject traversal-shaped filenames before any filesystem call
- In clients, treat 404 as 'not ready' with one bounded retry
When it happens
Trigger: Calling the download URL before the async generation finished writing the file; artifact generation failed silently so only the URL exists; passing a guessed filename directly to /api/outputs/...; server restarted against a different OUTPUTS_DIR (cwd-relative) where old artifacts are absent.
Common situations: Frontend clicking 'download' immediately after receiving the answer payload while the writer is still flushing; OUTPUTS_DIR resolved relative to a changed working directory between runs; artifacts cleaned up by a temp-dir policy; filename with URL-encoded characters not matching the stored name.
Related errors
AI-assisted analysis of datawhalechina/hello-agents@606a07d341 (2026-08-14).
Data as JSON: /api/errors/7c8ed3d5d7fa4a0e.
Report an issue: GitHub.