{"record":{"id":"1bcc5b40bf3bcdce","repo":"moeru-ai/airi","slug":"extension-asset-request-invalid","errorCode":"EXTENSION_ASSET_REQUEST_INVALID","errorMessage":"Unauthorized","messagePattern":"Unauthorized","errorType":"http","errorClass":"HttpError","httpStatus":401,"severity":"error","filePath":"apps/stage-tamagotchi/src/main/services/airi/http-server/static-assets/route.ts","lineNumber":63,"sourceCode":"      Object.entries(staticAssetSecurityHeaders).forEach(([key, value]) => {\n        event.res.headers.set(key, value)\n      })\n\n      if (event.req.method !== 'GET' && event.req.method !== 'HEAD') {\n        throw new HttpError({\n          status: 405,\n          code: 'EXTENSION_ASSET_METHOD_NOT_ALLOWED',\n          message: 'Method Not Allowed',\n        })\n      }\n\n      const requestPath = parseStaticAssetRequestPath(getRequestURL(event).pathname)\n      const extensionId = requestPath?.extensionId ?? ''\n      const assetSessionId = requestPath?.assetSessionId ?? ''\n      const assetPath = normalizeStaticAssetPath(requestPath?.assetPath ?? '')\n\n      if (!extensionId || !assetSessionId || !assetPath) {\n        throw new HttpError({\n          status: 401,\n          code: 'EXTENSION_ASSET_REQUEST_INVALID',\n          message: 'Unauthorized',\n          reason: 'required extensionId, assetSessionId, or assetPath is missing',\n        })\n      }\n\n      const cookieValue = getCookie(event, createStaticAssetSessionCookieName(assetSessionId))\n      const auth = await options.authorize({\n        extensionId,\n        assetSessionId,\n        assetPath,\n        cookieValue,\n      })\n      if (!auth.ok) {\n        throw auth.error\n      }\n","sourceCodeStart":45,"sourceCodeEnd":81,"githubUrl":"https://github.com/moeru-ai/airi/blob/27111382b4a79a7e983289d6e983a06af185ed0f/apps/stage-tamagotchi/src/main/services/airi/http-server/static-assets/route.ts#L45-L81","documentation":"This HttpError (status 401, code EXTENSION_ASSET_REQUEST_INVALID) is thrown by the extension static-asset HTTP route when the parsed request path is missing one or more of the three required segments: extensionId, assetSessionId, or assetPath. The route parses the incoming URL via parseStaticAssetRequestPath and defaults each segment to an empty string; if any remains empty the request is rejected as unauthorized because the server cannot correlate it to a valid asset session.","triggerScenarios":"A GET request to the static-asset endpoint whose URL path does not match the expected /<extensionId>/<assetSessionId>/<assetPath...> shape — e.g. a truncated URL, a malformed bookmark/iframe src, or a request constructed by code that forgot to include the assetSessionId segment produced by createAssetSession.","commonSituations":"An extension renders an asset URL with a missing piece (extensionId unknown at render time), a browser tab holding a stale URL after the session expired, manual testing with a hand-typed URL, or a bug in buildMountedStaticAssetPath producing an empty component.","solutions":["Inspect the failing request URL in the server logs and confirm all three path segments (extensionId, assetSessionId, assetPath) are present and non-empty.","Trace the URL back to its producer (buildMountedStaticAssetPath / createAssetSession return value) and ensure none of the inputs fed to it are undefined or empty.","Verify the asset session still exists and that the cookie name derived from createStaticAssetSessionCookieName(assetSessionId) matches what the client sends.","If the URL comes from persisted state (bookmarks, saved HTML), invalidate and regenerate it from a fresh createAssetSession call."],"exampleFix":"// before\nconst src = `/static-assets/${extensionId}/${assetPath}`\n\n// after\nimport { buildMountedStaticAssetPath } from '...'\nconst src = buildMountedStaticAssetPath({\n  extensionId,\n  assetSessionId: session.assetSessionId,\n  assetPath,\n})\nif (!src) throw new Error('asset path could not be built — check inputs')","handlingStrategy":"validation","validationCode":"// Validate the three required segments before constructing/requesting the asset URL\nfunction isValidAssetRequest(p: { extensionId?: string, assetSessionId?: string, assetPath?: string }): boolean {\n  return Boolean(p.extensionId && p.assetSessionId && p.assetPath)\n}\n\nif (!isValidAssetRequest({ extensionId, assetSessionId, assetPath })) {\n  throw new Error('Cannot request asset: extensionId, assetSessionId, and assetPath are all required')\n}","typeGuard":"function isCompleteAssetRef(ref: unknown): ref is { extensionId: string, assetSessionId: string, assetPath: string } {\n  return typeof ref === 'object' && ref !== null\n    && typeof (ref as any).extensionId === 'string' && (ref as any).extensionId !== ''\n    && typeof (ref as any).assetSessionId === 'string' && (ref as any).assetSessionId !== ''\n    && typeof (ref as any).assetPath === 'string' && (ref as any).assetPath !== ''\n}","tryCatchPattern":null,"preventionTips":["Always build asset URLs through buildMountedStaticAssetPath rather than string concatenation so segments can't be silently dropped.","Treat a falsy return from buildMountedStaticAssetPath as an error at the producer side, not the request side.","Invalidate persisted asset URLs when the underlying session changes."],"tags":["http","authentication","extensions","static-assets"],"backgroundTag":null,"analyzedSha":"27111382b4a79a7e983289d6e983a06af185ed0f","analyzedAt":"2026-08-12T18:33:34.132Z","schemaVersion":2},"datasetVersion":"2026-08-12T23:17:12.415Z"}