HeyPuter/puter · error · HttpError

bad_request

bad_request

Error message

Exactly one of `app` and `origin` is required

What it means

Thrown by GET /app-feedback/target when the XOR check `!app === !origin` fails — i.e. both app and origin are provided, or neither is. The endpoint takes exactly one target identifier: an app uid/name OR an origin URL. readTargetParam also returns undefined for values over 2048 chars or non-strings, so an over-long origin looks like 'not provided'.

Source

Thrown at src/backend/controllers/feedback/AppFeedbackController.ts:77

     * app accepts feedback, plus its canonical title/name for display. Reveals
     * nothing that `puter.apps.get` doesn't already.
     */
    @Get('/target', {
        subdomain: 'api',
        requireUserActor: true,
        requireVerified: true,
        rateLimit: {
            scope: 'app-feedback-target',
            limit: 60,
            window: 60_000,
            key: 'user',
        },
    })
    async target(req: Request, res: Response): Promise<void> {
        const app = readTargetParam(req.query.app);
        const origin = readTargetParam(req.query.origin);
        if (!app === !origin) {
            throw new HttpError(
                400,
                'Exactly one of `app` and `origin` is required',
                { legacyCode: 'bad_request' },
            );
        }

        const service = this.services.appFeedback as AppFeedbackService;
        res.json(await service.getTarget({ app, origin }));
    }

    /**
     * POST /app-feedback — store one feedback message and email the app's
     * developer. Strict limits: the route limits below are the cheap first
     * line; AppFeedbackService enforces durable per-user/per-app daily caps
     * from the database (the route limiter fails open, the DB caps don't).
     */
    @Post('/', {
        subdomain: 'api',

View on GitHub (pinned to 908ec23eda)

Solutions

  1. Send exactly one of ?app=<app> or ?origin=<origin>, not both, not neither.
  2. Ensure origin (if used) is a real URL under 2048 characters — longer values are dropped by readTargetParam and treated as absent.
  3. In client code, branch: pick app when you have an app id, otherwise origin, never concatenate.

Example fix

// before
fetch('/app-feedback/target?app=' + appId + '&origin=' + origin)
// after
fetch('/app-feedback/target?' + (appId ? 'app=' + appId : 'origin=' + origin))
Defensive patterns

Strategy: validation

Validate before calling

const params = appId ? { app: appId } : (origin ? { origin } : null);
if (!params) throw new Error('need app or origin');
// origin must be <= 2048 chars
if ('origin' in params && params.origin.length > 2048) throw new Error('origin too long');

Type guard

const hasOneTarget = (app?: string, origin?: string): boolean =>
  !!app !== !!origin; // exactly one

Prevention

When it happens

Trigger: GET /app-feedback/target?app=X&origin=Y (both), or /app-feedback/target with neither; an origin longer than 2048 chars (TARGET_PARAM_MAX_LENGTH) that readTargetParam silently drops to undefined, making an otherwise-present origin count as missing.

Common situations: Dialog that fills both fields from context; client always sending origin as a fallback alongside app; an origin with a huge tracking suffix exceeding the 2048 cap and being silently ignored.

Related errors


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