HeyPuter/puter · error · HttpError

bad_request

bad_request

Error message

Failed to batch write all items to DynamoDB

What it means

App names are globally unique. create() checks existsByName(fields.name) and, when a match is found and options.dedupe_name is NOT set, throws 400 app_name_already_in_use with the user-facing message. This is the default path; dedupe_name is opt-in.

Source

Thrown at src/backend/clients/dynamodb/DDBClient.ts:312

                if (Object.keys(unprocessedItems).length === 0) {
                    requestItems = {};
                    break;
                }

                requestItems = unprocessedItems as NonNullable<
                    BatchWriteCommandInput['RequestItems']
                >;
                if (attempt < MAX_BATCH_WRITE_RETRIES) {
                    const delayMs = Math.min(
                        1000,
                        BATCH_WRITE_RETRY_BASE_MS * 2 ** attempt,
                    );
                    await sleep(delayMs);
                }
            }

            if (Object.keys(requestItems).length > 0) {
                throw new HttpError(
                    400,
                    'Failed to batch write all items to DynamoDB',
                    { legacyCode: 'bad_request' },
                );
            }
        }

        return {
            ConsumedCapacity: Array.from(consumedCapacityByTable.entries()).map(
                ([TableName, CapacityUnits]) => ({
                    TableName,
                    CapacityUnits,
                }),
            ),
        };
    }

    @Span('ddb.del', (table: string) => ({ 'db.table': table }))

View on GitHub (pinned to 908ec23eda)

Solutions

  1. Choose a unique name (include a namespace prefix or random suffix).
  2. Pass options.dedupe_name:true to let the driver auto-suffix (beware the dedupe backstop above).
  3. Re-use the existing app via update() instead of create() if it is the same app.
  4. select() by name first to detect the collision and branch accordingly.

Example fix

// before
await puter.apps.create({ name: 'dashboard' });

// after — branch on existence
const existing = await puter.apps.list({ predicate: ['user-can-edit'] });
const mine = existing.find(a => a.name === 'dashboard');
if (mine) await puter.apps.update(mine.uid, { index_url });
else await puter.apps.create({ name: 'dashboard', index_url });
Defensive patterns

Strategy: validation

Validate before calling

// Check the name is free before create().
const taken = await puter.apps.list({ predicate: ['user-can-edit'] });
const nameExists = taken.some(a => a.name === candidate);
if (nameExists) { /* pick another or update() the existing */ }

Try / catch

try { await puter.apps.create({ name }); }
catch (e) {
  if (e?.code === 'app_name_already_in_use') { await puter.apps.create({ name }, { dedupe_name: true }); }
  else throw e;
}

Prevention

When it happens

Trigger: Calling puter.apps.create({ name }) where `name` is already taken by any app in the system (owned by anyone). Most common on retries/re-imports or when picking a generic name.

Common situations: Re-running a setup script that already created the app; choosing a common name like 'admin' or 'dashboard'; an old app row was not deleted before re-creating.

Related errors


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