TryGhost/Ghost · error · Error

Failed to update ${entityType}: ${response.status()}

Error message

Failed to update ${entityType}: ${response.status()}

What it means

Thrown by ApiPersistenceAdapter.update() when the PUT to {endpoint}/{id} returns non-ok. update() first does a findById() (so 404 is already handled upstream as error 93) then merges and PUTs the full object; this throw fires only on the PUT itself failing. The message carries the status code but not the body.

Source

Thrown at e2e/data-factory/persistence/adapters/api.ts:77

        }

        if (!response.ok()) {
            throw new Error(`Failed to find ${entityType}: ${response.status()}`);
        }

        const body = await response.json() as TResponse;
        return this.transformResponse(body) as T;
    }

    async update<T>(entityType: string, id: string, data: Partial<T>): Promise<T> {
        const existing = await this.findById<T>(entityType, id);

        const response = await this.httpClient.put(this.buildUrl(id), {
            data: this.transformRequest({...existing, ...data} as unknown as TRequest)
        });

        if (!response.ok()) {
            throw new Error(`Failed to update ${entityType}: ${response.status()}`);
        }

        const body = await response.json() as TResponse;
        return this.transformResponse(body) as T;
    }

    async delete(entityType: string, id: string): Promise<void> {
        const response = await this.httpClient.delete(this.buildUrl(id));

        if (!response.ok() && response.status() !== 404) {
            throw new Error(`Failed to delete ${entityType}: ${response.status()}`);
        }
    }
}

View on GitHub (pinned to 47d8b0e2ad)

Solutions

  1. Inspect the status code: 403 → permission; 422 → validation (check which field); 409 → conflict; 500 → server log.
  2. Send only the fields you intend to change — the adapter merges onto the full existing record, so stray fields from build() can cause validation failures; consider narrowing the partial.
  3. Confirm the test role has edit permission on the resource.
  4. If it's a uniqueness collision, generate a unique value (e.g. random slug) in the partial.
  5. Run 'pnpm test:types' to ensure the partial's shape matches the API contract.

Example fix

// before: merges the entire built record, may carry fields invalid on edit
await factory.update(post.id, factory.build({status: 'published'}));

// after: send only the changed field as a narrow partial
await factory.update(post.id, {status: 'published'});
Defensive patterns

Strategy: validation

Validate before calling

// Send narrow partials — don't merge a full built record onto the update
const narrow = {status: 'published'}; // only the field(s) you intend to change
await factory.update(post.id, narrow);

Try / catch

try {
    return await factory.update(id, patch);
} catch (err) {
    const status = /Failed to update .*: (\d+)/.exec(err.message)?.[1];
    if (status === '422') throw new Error('Validation failed on update — check unique fields in the patch');
    throw err;
}

Prevention

When it happens

Trigger: The merged payload fails server-side validation (e.g. a field constraint, a slug collision with another entity, an immutable field being changed); the test role lacks write permission (403); optimistic-concurrency conflict; or the Ghost server errored on the update.

Common situations: Factory update overwrote a unique field (slug, email) with a colliding value; the merged {...existing, ...data} included fields the API rejects on edit; permissions changed between create and update; another worker mutated the same record causing a conflict.

Related errors


AI-assisted analysis of TryGhost/Ghost@47d8b0e2ad (2026-08-13). Data as JSON: /api/errors/a568a8ad3fa97dc5. Report an issue: GitHub.