lobehub/lobehub · error · TRPCError

FORBIDDEN

FORBIDDEN

Error message

Invalid Request!

What it means

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.

Source

Thrown at apps/server/src/routers/async/image.ts:115

      log('Starting async image generation: %O', {
        generationId,
        imageParams: {
          cfg: params.cfg,
          height: params.height,
          steps: params.steps,
          width: params.width,
        },
        model,
        prompt: params.prompt,
        provider,
        taskId,
      });

      // Check if generationBatch exists before processing
      const generationBatch = await generationBatchModel.findById(generationBatchId);
      if (!generationBatch) {
        log('Generation batch not found: %s, skipping image generation', generationBatchId);
        throw new TRPCError({ code: 'FORBIDDEN', message: 'Invalid Request!' });
      }

      // Billing context is loaded inside the guarded section below so that a
      // failure (e.g. a stale model mapping) still marks the task as Error and
      // reconciles the precharge handle; the error path falls back to identity
      // mapping when resolution itself is what failed.
      // requestedModelId is optional on the mapping result, so it must allow
      // undefined even though it defaults to the raw model id.
      let requestedModelId: string | undefined = model;
      let resolvedModelId = model;
      let prechargeResult: unknown;

      log('Updating task status to Processing: %s', taskId);
      await asyncTaskModel.update(taskId, { status: AsyncTaskStatus.Processing });

      // Use AbortController to prevent resource leaks
      const abortController = new AbortController();
      let timeoutId: ReturnType<typeof setTimeout> | null = null;

View on GitHub (pinned to 10f24d7ade)

Solutions

  1. Re-resolve generationBatchId for the current user before retrying; refresh the batches list in the UI.
  2. Confirm the authenticated user owns the batch — the FORBIDDEN code intentionally hides existence from non-owners.
  3. If the batch was deleted by retention, create a new batch and dispatch the image task against it.
  4. Audit the retention/cleanup job so it does not delete batches that still have pending image tasks.
  5. On the client, treat FORBIDDEN from this route as 'batch no longer available — refresh and retry'.

Example fix

// before
const generationBatch = await generationBatchModel.findById(generationBatchId);
if (!generationBatch) {
  log('Generation batch not found: %s, skipping image generation', generationBatchId);
  throw new TRPCError({ code: 'FORBIDDEN', message: 'Invalid Request!' });
}

// after — distinguish owner mismatch (FORBIDDEN) from missing (NOT_FOUND) for ops debugging while keeping user-facing message generic
const generationBatch = await generationBatchModel.findById(generationBatchId);
if (!generationBatch) {
  log('Generation batch not found: %s, skipping image generation', generationBatchId);
  throw new TRPCError({ code: 'FORBIDDEN', message: 'Invalid Request!' });
}
if (generationBatch.userId !== ctx.userId) {
  throw new TRPCError({ code: 'FORBIDDEN', message: 'Invalid Request!' });
}
Defensive patterns

Strategy: validation

Validate before calling

const batch = await generationBatchModel.findById(generationBatchId);
if (!batch || batch.userId !== ctx.userId) {
  // FORBIDDEN is intentional — do not reveal existence to non-owners; refresh the batch list instead
  throw new TRPCError({ code: 'FORBIDDEN', message: 'Invalid Request!' });
}

Type guard

function isGenerationBatchRow(value: unknown): value is { id: string; userId: string } {
  return typeof value === 'object' && value !== null && typeof (value as any).id === 'string' && typeof (value as any).userId === 'string';
}

Try / catch

try {
  await trpc.async.image.generate.mutate(input);
} catch (e) {
  if (e instanceof TRPCError && e.code === 'FORBIDDEN') {
    // batch is gone or not owned — refresh the UI list and stop retrying with the same id
    refreshBatches();
  } else throw e;
}

Prevention

When it happens

Trigger: 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.

Common situations: 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.

Related errors


AI-assisted analysis of lobehub/lobehub@10f24d7ade (2026-08-12). Data as JSON: /api/errors/eb8345c1bfa05b5f. Report an issue: GitHub.