HeyPuter/puter · error · HttpError

bad_request

bad_request

Error message

Missing or invalid `name` query param

What it means

AppDriver.select enforces a single pagination mode per request. If args.cursor decodes to a payload AND args.offset is also defined, it throws 400 bad_request. Mixing cursor- and offset-based pagination is ambiguous (cursor already encodes position), so the driver refuses rather than guessing which wins. See doc/pagination.md for the one convention.

Source

Thrown at src/backend/controllers/apps/AppController.js:175

        router.get(
            '/apps/nameAvailable',
            {
                subdomain: 'api',
                requireAuth: true,
                // Answers "does this name exist?" for any name, so it is a
                // name-enumeration oracle however cheap it is to serve.
                // Mirrors the `isNameAvailable` budget on AppDriver.
                rateLimit: {
                    scope: 'app-name-available',
                    limit: 60,
                    window: 60_000,
                    key: 'user',
                },
            },
            async (req, res) => {
                const name = req.query?.name;
                if (!name || typeof name !== 'string') {
                    throw new HttpError(
                        400,
                        'Missing or invalid `name` query param',
                        { legacyCode: 'bad_request' },
                    );
                }
                const available = await this.appDriver.isNameAvailable(name);
                res.json({ name, available });
            },
        );

        // POST /rao — record a recent app open. When an app-under-user
        // actor calls this, the app id is already on the token — clients
        // don't re-send it in the body. Fall back to `actor.app.uid`
        // before 400-ing for a missing body field.
        //
        // Authorization: only two callers are trusted to report opens —
        //   1. a root user actor (plain session, no `.app` and no access
        //      token), e.g. the GUI launching apps on behalf of the user;

View on GitHub (pinned to 908ec23eda)

Solutions

  1. Send exactly one of cursor or offset — never both.
  2. For 'next page', pass only the cursor returned by the previous response and drop offset.
  3. Reset offset to undefined when adopting cursor pagination in the client.
  4. Validate the params object before submit: assert !(cursor && offset != null).

Example fix

// before
await puter.apps.list({ cursor: prevCursor, offset: 0, limit: 50 });

// after — pick one pagination mode
await puter.apps.list({ cursor: prevCursor, limit: 50 });
// or
await puter.apps.list({ offset: 100, limit: 50 });
Defensive patterns

Strategy: validation

Validate before calling

function assertOnePager(args) {
  if (args.cursor != null && args.offset != null) {
    throw new Error('cursor and offset cannot be combined');
  }
}
assertOnePager(params);

Prevention

When it happens

Trigger: Calling puter.apps.list() / the driver select() with both `cursor` (from a previous page) and `offset` set in the same request — e.g. a UI that held an offset default while also forwarding the server's cursor.

Common situations: Client merged a cursor-based 'next page' flow with a legacy offset-based default; query-param builder always includes offset=0; refactor left an offset field populated alongside the cursor.

Related errors


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