{"record":{"id":"eb8345c1bfa05b5f","repo":"lobehub/lobehub","slug":"forbidden","errorCode":"FORBIDDEN","errorMessage":"Invalid Request!","messagePattern":"Invalid Request!","errorType":"error_code","errorClass":"TRPCError","httpStatus":403,"severity":"error","filePath":"apps/server/src/routers/async/image.ts","lineNumber":115,"sourceCode":"      log('Starting async image generation: %O', {\n        generationId,\n        imageParams: {\n          cfg: params.cfg,\n          height: params.height,\n          steps: params.steps,\n          width: params.width,\n        },\n        model,\n        prompt: params.prompt,\n        provider,\n        taskId,\n      });\n\n      // Check if generationBatch exists before processing\n      const generationBatch = await generationBatchModel.findById(generationBatchId);\n      if (!generationBatch) {\n        log('Generation batch not found: %s, skipping image generation', generationBatchId);\n        throw new TRPCError({ code: 'FORBIDDEN', message: 'Invalid Request!' });\n      }\n\n      // Billing context is loaded inside the guarded section below so that a\n      // failure (e.g. a stale model mapping) still marks the task as Error and\n      // reconciles the precharge handle; the error path falls back to identity\n      // mapping when resolution itself is what failed.\n      // requestedModelId is optional on the mapping result, so it must allow\n      // undefined even though it defaults to the raw model id.\n      let requestedModelId: string | undefined = model;\n      let resolvedModelId = model;\n      let prechargeResult: unknown;\n\n      log('Updating task status to Processing: %s', taskId);\n      await asyncTaskModel.update(taskId, { status: AsyncTaskStatus.Processing });\n\n      // Use AbortController to prevent resource leaks\n      const abortController = new AbortController();\n      let timeoutId: ReturnType<typeof setTimeout> | null = null;","sourceCodeStart":97,"sourceCodeEnd":133,"githubUrl":"https://github.com/lobehub/lobehub/blob/10f24d7ade75139093a9373b364f6bc91f3cd7db/apps/server/src/routers/async/image.ts#L97-L133","documentation":"FORBIDDEN TRPC error raised inside the image-generation worker when GenerationBatchModel.findById(generationBatchId) returns null. FORBIDDEN (not NOT_FOUND) is used deliberately — a missing batch is treated as an authorisation failure because batches are user-scoped and should never be addressable by an unauthorised caller. The generic 'Invalid Request!' message avoids leaking whether the batch exists.","triggerScenarios":"Client dispatches an image-generation task against a generationBatchId that was deleted, belongs to another user, or never existed; retry of a task after the parent batch was garbage-collected; race between batch deletion and the worker polling the queue.","commonSituations":"Cross-user batch id leak; batch auto-pruned before the image job ran; UI retry button reusing a stale id; integration test that did not seed the batch row.","solutions":["Re-resolve generationBatchId for the current user before retrying; refresh the batches list in the UI.","Confirm the authenticated user owns the batch — the FORBIDDEN code intentionally hides existence from non-owners.","If the batch was deleted by retention, create a new batch and dispatch the image task against it.","Audit the retention/cleanup job so it does not delete batches that still have pending image tasks.","On the client, treat FORBIDDEN from this route as 'batch no longer available — refresh and retry'."],"exampleFix":"// before\nconst generationBatch = await generationBatchModel.findById(generationBatchId);\nif (!generationBatch) {\n  log('Generation batch not found: %s, skipping image generation', generationBatchId);\n  throw new TRPCError({ code: 'FORBIDDEN', message: 'Invalid Request!' });\n}\n\n// after — distinguish owner mismatch (FORBIDDEN) from missing (NOT_FOUND) for ops debugging while keeping user-facing message generic\nconst generationBatch = await generationBatchModel.findById(generationBatchId);\nif (!generationBatch) {\n  log('Generation batch not found: %s, skipping image generation', generationBatchId);\n  throw new TRPCError({ code: 'FORBIDDEN', message: 'Invalid Request!' });\n}\nif (generationBatch.userId !== ctx.userId) {\n  throw new TRPCError({ code: 'FORBIDDEN', message: 'Invalid Request!' });\n}","handlingStrategy":"validation","validationCode":"const batch = await generationBatchModel.findById(generationBatchId);\nif (!batch || batch.userId !== ctx.userId) {\n  // FORBIDDEN is intentional — do not reveal existence to non-owners; refresh the batch list instead\n  throw new TRPCError({ code: 'FORBIDDEN', message: 'Invalid Request!' });\n}","typeGuard":"function isGenerationBatchRow(value: unknown): value is { id: string; userId: string } {\n  return typeof value === 'object' && value !== null && typeof (value as any).id === 'string' && typeof (value as any).userId === 'string';\n}","tryCatchPattern":"try {\n  await trpc.async.image.generate.mutate(input);\n} catch (e) {\n  if (e instanceof TRPCError && e.code === 'FORBIDDEN') {\n    // batch is gone or not owned — refresh the UI list and stop retrying with the same id\n    refreshBatches();\n  } else throw e;\n}","preventionTips":["Client-side, treat FORBIDDEN from this route as 'batch no longer available — refresh'.","Confirm ownership of generationBatchId before queueing image tasks.","Audit retention/cleanup so it does not delete batches with pending image tasks.","Re-create the batch and dispatch with the new id rather than reusing a stale one."],"tags":["trpc","image-generation","generation-batch","authorization","async-task","server"],"backgroundTag":null,"analyzedSha":"10f24d7ade75139093a9373b364f6bc91f3cd7db","analyzedAt":"2026-08-12T11:43:19.543Z","schemaVersion":2},"datasetVersion":"2026-08-12T18:17:37.767Z"}