{"record":{"id":"80d9dd603c64fe5c","repo":"unslothai/unsloth","slug":"invalid-or-expired-video-link","errorCode":null,"errorMessage":"Invalid or expired video link.","messagePattern":"Invalid or expired video link\\.","errorType":"http","errorClass":"HTTPException","httpStatus":401,"severity":"error","filePath":"studio/backend/routes/video.py","lineNumber":514,"sourceCode":"    Returned as a relative URL so it works behind any proxy the page itself is served through.\"\"\"\n    from core.inference import video_gallery\n\n    path = await asyncio.to_thread(video_gallery.owned_video_path, video_id)\n    if path is None:\n        raise HTTPException(status_code = 404, detail = \"Video not found.\")\n    token = _sign_video_id(video_id)\n    return {\"url\": f\"/api/inference/video/gallery/{video_id}/file-signed?token={token}\"}\n\n\n@router.get(\"/video/gallery/{video_id}/file-signed\")\nasync def get_gallery_video_file_signed(video_id: str, token: str = Query(...)):\n    \"\"\"Stream one gallery MP4 gated by the HMAC token instead of the bearer, so it can be a plain\n    <video src> and the browser can range-request it. Same ownership gate as the bearer route, and\n    the token names the single clip it may serve.\"\"\"\n    from core.inference import video_gallery\n\n    if _verify_video_link_token(token) != video_id:\n        raise HTTPException(status_code = 401, detail = \"Invalid or expired video link.\")\n    path = await asyncio.to_thread(video_gallery.owned_video_path, video_id)\n    if path is None:\n        raise HTTPException(status_code = 404, detail = \"Video not found.\")\n    from fastapi.responses import FileResponse\n\n    return FileResponse(\n        path,\n        media_type = \"video/mp4\",\n        headers = {\"Cache-Control\": \"private, max-age=31536000, immutable\"},\n    )\n\n\n@router.get(\"/video/gallery/{video_id}/export\")\nasync def export_gallery_video(\n    video_id: str,\n    format: str = \"webm\",\n    current_subject: str = Depends(get_current_subject),\n):","sourceCodeStart":496,"sourceCodeEnd":532,"githubUrl":"https://github.com/unslothai/unsloth/blob/203007d19051dcd2ae33876786d117c99f6b0368/studio/backend/routes/video.py#L496-L532","documentation":"Raised by the signed gallery-video file route when the HMAC token query parameter fails verification or names a different video_id than the one in the URL path. The token is produced by _sign_video_id(video_id) and is short-lived, so an expired or copy-pasted/mangled link fails here. It is a 401 because the token IS the credential on this route (no bearer header; the URL must work as a plain <video src>).","triggerScenarios":"GET /api/inference/video/gallery/{video_id}/file-signed?token=... where (a) the token expired, (b) the token was signed for a different video_id (e.g. template reused a stale URL after the gallery refreshed), (c) the token string was truncated/re-encoded by the frontend, or (d) the server restarted with a new signing secret, invalidating all previously issued links.","commonSituations":"A gallery page left open past the token TTL; a cached HTML/JS bundle embedding old signed URLs; a reverse proxy or middleware stripping/mangling the query string; server restart rotating the HMAC secret.","solutions":["Re-fetch the gallery list so fresh signed URLs are minted, then retry the request.","If the page persists URLs, store only the video_id and request a fresh signed URL on demand instead of persisting the token.","Check that the frontend passes the token unescaped (use encodeURIComponent on the query param).","If links die across restarts, make the signing secret persistent (env/config) rather than per-process."],"exampleFix":"// before (persisted URL goes stale)\nconst url = clip.signedUrl; // stored days ago\n\n// after (mint on demand)\nconst { url } = await api.get(`/video/gallery/${clip.id}/signed-url`);\nvideoEl.src = url;","handlingStrategy":"retry","validationCode":"// Re-mint the signed URL right before assigning it to <video>\nasync function freshSignedUrl(videoId) {\n  const r = await fetch(`/api/inference/video/gallery/${videoId}/signed-url`, { headers: auth });\n  if (r.status === 404) return null; // clip gone\n  const { url } = await r.json();\n  return url;\n}","typeGuard":"function isSignedUrl(u: string): boolean {\n  try { const q = new URL(u, location.origin).searchParams; return !!q.get('token'); }\n  catch { return false; }\n}","tryCatchPattern":"videoEl.onerror = async () => {\n  const fresh = await freshSignedUrl(videoId);\n  if (fresh) videoEl.src = fresh;      // token expired: remint once\n  else removeClipFromUI(videoId);      // clip gone: drop card\n};","preventionTips":["Never persist signed URLs across sessions; store video_id only","Mint the signed URL at render time, not at list-fetch time, when TTLs are short","Keep the signing secret stable across restarts if links must survive them"],"tags":["http-401","hmac","signed-url","video-streaming","fastapi"],"backgroundTag":null,"analyzedSha":"203007d19051dcd2ae33876786d117c99f6b0368","analyzedAt":"2026-08-15T02:48:39.846Z","schemaVersion":2},"datasetVersion":"2026-08-15T17:31:12.345Z"}