HeyPuter/puter · error · HttpError

App owner not found

Error message

App owner not found

What it means

GeminiVideoProvider.generate validates that `prompt` is a non-empty string before resolving the model or honoring test_mode. Anything that is not a string, or a string of only whitespace, is rejected with 400 bad_request. This is the first guard in generate() and runs unconditionally.

Source

Thrown at extensions/appTelemetry.ts:132

            max: MAX_LIMIT,
            fallback: DEFAULT_LIMIT,
        });
        const safeOffset = parseIntParam(offset, {
            key: 'offset',
            min: 0,
            max: MAX_OFFSET,
            fallback: 0,
        });

        const app = await this.stores.app.getByUid(app_uuid);
        if (!app) throw new HttpError(404, 'App not found');

        // The `apps-of-user:<uuid>:write` implicator keys on the owner's
        // UUID, not the numeric id. Look up the owner explicitly — the raw
        // app row only carries `owner_user_id`. (v1 got the owner for free
        // because its entity-storage layer eager-joined the owner row.)
        const ownerId = (app as { owner_user_id?: number }).owner_user_id;
        if (!ownerId) throw new HttpError(404, 'App owner not found');
        const owner = await this.stores.user.getById(ownerId);
        if (!owner?.uuid) throw new HttpError(404, 'App owner not found');

        const actor = Context.get('actor');
        if (!actor) throw new HttpError(401, 'Authentication required');
        const ownsApp = await this.services.permission
            .check(actor as Actor, `apps-of-user:${owner.uuid}:write`)
            .catch(() => false);
        if (!ownsApp) throw new HttpError(403, 'Permission denied');

        const appId = (app as { id: number }).id;

        const users = (await this.clients.db.read(
            `SELECT u.id, u.username, u.uuid, u.email FROM user_to_app_permissions p
             INNER JOIN ${this.clients.db.quoteIdentifier('user')} u ON p.user_id = u.id
             WHERE p.permission = 'flag:app-is-authenticated' AND p.app_id = ?
             ORDER BY (p.dt IS NOT NULL), p.dt, p.user_id
             LIMIT ? OFFSET ?`,

View on GitHub (pinned to 908ec23eda)

Solutions

  1. Ensure the prompt is a trimmed non-empty string before calling generate().
  2. Disable the submit action in the UI until the prompt field has non-whitespace content.
  3. Coerce untrusted input with `String(value).trim()` and reject empty upstream of the API.
  4. Cross-check the param order against IGenerateVideoParams — prompt belongs under the `prompt` key.

Example fix

// before
await gemini.generate({ prompt: userInput?.text ?? '' });
await gemini.generate({ prompt: null });

// after
const prompt = (userInput?.text ?? '').trim();
if (!prompt) throw new Error('prompt required');
await gemini.generate({ prompt });
Defensive patterns

Strategy: validation

Validate before calling

function cleanPrompt(p) {
  if (typeof p !== 'string') throw new Error('prompt must be a non-empty string');
  const trimmed = p.trim();
  if (!trimmed) throw new Error('prompt must be a non-empty string');
  return trimmed;
}
const prompt = cleanPrompt(userInput);

Type guard

function isNonEmptyPrompt(p) {
  return typeof p === 'string' && p.trim().length > 0;
}

Prevention

When it happens

Trigger: Calling the Gemini video generate path with prompt omitted, null, undefined, a number/object, or an empty/whitespace string. Commonly a missing form field, a default value never set, or prompt built from an empty template literal.

Common situations: Frontend submits the form before the user typed a prompt; prompt sourced from a config/env var that is unset; prompt passed positionally in the wrong slot of the params object.

Related errors


AI-assisted analysis of HeyPuter/puter@908ec23eda (2026-08-12). Data as JSON: /api/errors/6709d9cd32c010d5. Report an issue: GitHub.