{"record":{"id":"a3e28fa72b9537bc","repo":"moeru-ai/airi","slug":"extension-asset-method-not-allowed","errorCode":"EXTENSION_ASSET_METHOD_NOT_ALLOWED","errorMessage":"Method Not Allowed","messagePattern":"Method Not Allowed","errorType":"http","errorClass":"HttpError","httpStatus":405,"severity":"warning","filePath":"apps/stage-tamagotchi/src/main/services/airi/http-server/static-assets/route.ts","lineNumber":50,"sourceCode":" * Use when:\n * - Serving plugin iframe assets under `/_airi/extensions/:extensionId/sessions/:assetSessionId/ui/**assetPath`\n *\n * Expects:\n * - Cookie-backed asset session data to be present and valid\n * - `resolveAsset` to map request params into a validated local file\n *\n * Returns:\n * - H3 event handler that enforces cookie auth before static file response\n */\nexport function createStaticAssetRoute(options: StaticAssetRouteOptions) {\n  return eventHandler(async (event) => {\n    try {\n      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        })","sourceCodeStart":32,"sourceCodeEnd":68,"githubUrl":"https://github.com/moeru-ai/airi/blob/27111382b4a79a7e983289d6e983a06af185ed0f/apps/stage-tamagotchi/src/main/services/airi/http-server/static-assets/route.ts#L32-L68","documentation":"An HttpError with status 405 thrown by the extension static-asset route handler when the request method is neither GET nor HEAD. The route exists to serve plugin iframe assets (/_airi/extensions/:extensionId/sessions/:assetSessionId/ui/**assetPath) and intentionally rejects mutating verbs before any auth/asset resolution. The stable code EXTENSION_ASSET_METHOD_NOT_ALLOWED lets callers distinguish it from other 405s.","triggerScenarios":"Any POST/PUT/DELETE/PATCH/OPTIONS request hitting the static-asset route URL. Because the check runs after security headers are set but before auth and path parsing, it fires uniformly for all non-GET/HEAD methods regardless of credentials.","commonSituations":"A plugin iframe host posting a form to an asset URL by mistake; a reverse proxy health check using OPTIONS; a misconfigured client treating the asset endpoint as an upload target; browser preflight (OPTIONS) before a cross-origin request to the asset path.","solutions":["Issue only GET (or HEAD) requests to extension static-asset URLs; they serve files, not actions.","Point mutating plugin API calls at the dedicated extension API route, not the ui/** asset path.","If you need OPTIONS support for CORS preflight, add an explicit OPTIONS handler rather than relying on the asset route.","Inspect the request method and URL in the browser network panel to confirm the wrong verb/endpoint combination."],"exampleFix":"// before\nfetch(`/_airi/extensions/${extId}/sessions/${sid}/ui/index.html`, { method: 'POST' })\n\n// after\nfetch(`/_airi/extensions/${extId}/sessions/${sid}/ui/index.html`, { method: 'GET' })","handlingStrategy":"validation","validationCode":"function isAllowedAssetMethod(method: string): boolean {\n  return method === 'GET' || method === 'HEAD'\n}\n\nif (!isAllowedAssetMethod(request.method)) {\n  // point the client at the extension API route instead\n}","typeGuard":"function isAllowedAssetMethod(method: string): boolean {\n  return method === 'GET' || method === 'HEAD'\n}","tryCatchPattern":"try {\n  await fetch(assetUrl, { method })\n}\ncatch (error) {\n  if (error?.code === 'EXTENSION_ASSET_METHOD_NOT_ALLOWED') {\n    // switch to GET, or route the call to the extension API endpoint\n  }\n}","preventionTips":["Use only GET/HEAD for extension static-asset URLs.","Route mutating plugin calls to the dedicated extension API route.","Add an explicit OPTIONS handler if CORS preflight is needed on the asset path."],"tags":["http","h3","static-assets","extensions","method-not-allowed","ipc","electron"],"backgroundTag":null,"analyzedSha":"27111382b4a79a7e983289d6e983a06af185ed0f","analyzedAt":"2026-08-12T18:33:34.132Z","schemaVersion":2},"datasetVersion":"2026-08-12T23:17:12.415Z"}