{"record":{"id":"7c8ed3d5d7fa4a0e","repo":"datawhalechina/hello-agents","slug":"report-artifact-not-found","errorCode":null,"errorMessage":"report artifact not found","messagePattern":"report artifact not found","errorType":"http","errorClass":"HTTPException","httpStatus":404,"severity":"warning","filePath":"Co-creation-projects/monkeyhlj-NetworkHealthReportAgent/src/api/main.py","lineNumber":99,"sourceCode":"\n@app.get(\"/api/reports\")\ndef get_all_site_reports(\n    start_date: str | None = Query(default=None, description=\"YYYY-MM-DD\"),\n    end_date: str | None = Query(default=None, description=\"YYYY-MM-DD\"),\n) -> dict:\n    start, end = default_date_window(start_date=start_date, end_date=end_date, days=7)\n    reports = []\n    for site in orchestrator.list_sites():\n        reports.append(orchestrator.build_report(site_id=site[\"site_id\"], start=start, end=end))\n\n    return {\"count\": len(reports), \"reports\": reports}\n\n\n@app.get(\"/api/outputs/{filename}\", name=\"download_generated_report\")\ndef download_generated_report(filename: str) -> FileResponse:\n    file_path = OUTPUTS_DIR / filename\n    if not file_path.exists() or not file_path.is_file():\n        raise HTTPException(status_code=404, detail=\"report artifact not found\")\n    return FileResponse(file_path, filename=filename, media_type=\"text/markdown; charset=utf-8\")\n\n\n@app.post(\"/api/chat\")\ndef ask_global_question(payload: AskRequest, request: Request) -> dict:\n    start, end = default_date_window(\n        start_date=payload.start_date,\n        end_date=payload.end_date,\n        days=7,\n    )\n    try:\n        answer = orchestrator.ask_global_question(\n            question=payload.question,\n            start=start,\n            end=end,\n            site_id=payload.site_id,\n        )\n        artifact = answer.get(\"artifact\")","sourceCodeStart":81,"sourceCodeEnd":117,"githubUrl":"https://github.com/datawhalechina/hello-agents/blob/606a07d341a47be773fab7f4b71177f53f96b2c3/Co-creation-projects/monkeyhlj-NetworkHealthReportAgent/src/api/main.py#L81-L117","documentation":"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.","triggerScenarios":"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.","commonSituations":"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.","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."],"exampleFix":"# before\n@app.get(\"/api/outputs/{filename}\")\ndef download_generated_report(filename: str) -> FileResponse:\n    file_path = OUTPUTS_DIR / filename\n# after\nfrom pathlib import Path\n@app.get(\"/api/outputs/{filename}\")\ndef download_generated_report(filename: str) -> FileResponse:\n    if \"/\" in filename or \"\\\\\" in filename or \"..\" in filename:\n        raise HTTPException(status_code=400, detail=\"invalid filename\")\n    file_path = (OUTPUTS_DIR / filename).resolve()\n    if OUTPUTS_DIR.resolve() not in file_path.parents:\n        raise HTTPException(status_code=400, detail=\"invalid filename\")\n    if not file_path.is_file():\n        raise HTTPException(status_code=404, detail=\"report artifact not found\")","handlingStrategy":"validation","validationCode":"from pathlib import Path\n\ndef safe_artifact_path(outputs_dir: Path, filename: str) -> Path | None:\n    if not filename or \"/\" in filename or \"\\\\\" in filename or \"..\" in filename:\n        return None\n    p = (outputs_dir / filename).resolve()\n    return p if p.parent == outputs_dir.resolve() and p.is_file() else None","typeGuard":null,"tryCatchPattern":"resp = requests.get(download_url)\nif resp.status_code == 404:\n    # artifact not yet written — retry once after generation completes\n    time.sleep(1)\n    resp = requests.get(download_url)\nresp.raise_for_status()","preventionTips":["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"],"tags":["fastapi","http-404","file-not-found","artifacts","path-traversal"],"backgroundTag":null,"analyzedSha":"606a07d341a47be773fab7f4b71177f53f96b2c3","analyzedAt":"2026-08-14T22:57:27.446Z","schemaVersion":2},"datasetVersion":"2026-08-15T22:17:37.221Z"}