{"record":{"id":"6ffa56bdeb77d55a","repo":"calesthio/OpenMontage","slug":"path-escapes-project","errorCode":null,"errorMessage":"path escapes project","messagePattern":"path escapes project","errorType":"http","errorClass":"HTTPException","httpStatus":403,"severity":"error","filePath":"backlot/server.py","lineNumber":251,"sourceCode":"                    yield _sse({\"type\": \"change\", \"project_id\": changed})\n            finally:\n                hub.unsubscribe(q)\n\n        return StreamingResponse(stream(), media_type=\"text/event-stream\", headers={\n            \"Cache-Control\": \"no-cache\",\n            \"X-Accel-Buffering\": \"no\",\n        })\n\n    # ---- Thumbnails (downscaled, cached on disk) ------------------------\n\n    @app.get(\"/thumb/{project_id}/{file_path:path}\")\n    async def thumb(project_id: str, file_path: str, w: int = 640) -> FileResponse:\n        project_dir = _safe_project_dir(project_id)\n        target = (project_dir / file_path).resolve()\n        try:\n            target.relative_to(project_dir.resolve())\n        except ValueError:\n            raise HTTPException(status_code=403, detail=\"path escapes project\")\n        if not target.is_file():\n            raise HTTPException(status_code=404, detail=\"media not found\")\n        width = min(THUMB_WIDTHS, key=lambda x: abs(x - w))\n        cached = await asyncio.to_thread(_thumbnail_for, target, width)\n        if cached is None:\n            # Never fall back to raw video bytes for an <img> consumer (F-03);\n            # non-thumbable images are safe to serve as-is.\n            if target.suffix.lower() in {\".mp4\", \".webm\", \".mov\"}:\n                raise HTTPException(status_code=404, detail=\"no poster frame available\")\n            return FileResponse(target)\n        return FileResponse(cached, media_type=\"image/jpeg\")\n\n    # ---- Media (range requests handled by FileResponse) ---------------\n\n    @app.get(\"/media/{project_id}/{file_path:path}\")\n    async def media(project_id: str, file_path: str) -> FileResponse:\n        project_dir = _safe_project_dir(project_id)\n        target = (project_dir / file_path).resolve()","sourceCodeStart":233,"sourceCodeEnd":269,"githubUrl":"https://github.com/calesthio/OpenMontage/blob/95e1c3d0ab93482159818560f6a8c8e866b9139f/backlot/server.py#L233-L269","documentation":"The final range check in _normalize_duration: the value parsed to an integer, but that integer is not -1 and not within 4..max_seconds (default 15). Note a separate earlier check also rejects non-canonical forms like \"4.5\"; this specific message fires only for genuinely out-of-range integers such as 3, 16, or 30.","triggerScenarios":"duration=3, duration=16, duration=0, or duration=-2 passed to seedance_ark. Also passing a longer duration (e.g. 20) intended for a different provider whose max is higher.","commonSituations":"Porting prompts/configs from another video model that allows 5-10s or up to 60s; assuming -1 means 'unlimited' and then also trying values like 60; off-by-one attempts at the boundary (duration=15 works, 16 does not).","solutions":["Clamp the requested duration into [4, 15] or send -1 to let Ark pick.","Check which model variant you target — max_seconds is a parameter (default 15); if the model supports longer clips, pass the correct max through the tool's configuration rather than guessing.","Split a long clip request into multiple 15s generations and concatenate."],"exampleFix":"# before\ninputs = {\"duration\": 30}\n# after\nMAX = 15\ninputs = {\"duration\": min(max(4, requested), MAX) if requested > 0 else -1}","handlingStrategy":"validation","validationCode":"def clamp_duration(requested, lo=4, hi=15):\n    if requested in (\"auto\", -1, None):\n        return -1\n    return max(lo, min(hi, int(requested)))","typeGuard":null,"tryCatchPattern":"try:\n    tool.run(inputs)\nexcept ValueError as e:\n    if \"duration must be between\" in str(e):\n        inputs[\"duration\"] = -1\n        tool.run(inputs)\n    else:\n        raise","preventionTips":["Clamp user/LLM-provided durations into 4-15 before calling.","Remember -1 delegates the choice to the model — use it instead of guessing."],"tags":["python","validation","duration","range","seedance"],"backgroundTag":null,"analyzedSha":"95e1c3d0ab93482159818560f6a8c8e866b9139f","analyzedAt":"2026-08-15T06:31:20.014Z","schemaVersion":2},"datasetVersion":"2026-08-15T22:17:37.221Z"}