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

  1. Verify the file exists server-side: ls $OUTPUTS_DIR and compare with the filename in the download_url.
  2. Retry the download once generation verifiably completed (poll file existence or wait for the response that includes the artifact before enabling the link).
  3. Make OUTPUTS_DIR absolute/configurable so restarts from other cwds do not orphan old artifacts.
  4. Ensure the generation code writes the file (and flushes/fsyncs) before attaching download_url to the response.
  5. 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

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.