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
- Inspect the status code: 403 → permission; 422 → validation (check which field); 409 → conflict; 500 → server log.
- 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.
- Confirm the test role has edit permission on the resource.
- If it's a uniqueness collision, generate a unique value (e.g. random slug) in the partial.
- 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
- Pass only changed fields to update(); the adapter already merges onto the fetched record.
- Generate unique values for constrained fields (slug, email) when updating.
- Confirm the test role has edit permission on the resource.
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
- Failed to create ${entityType}: ${response.status()} ${error
- ${entityType} with id ${id} not found
- Cannot create without a persistence adapter. Use buildMany()
- Failed to find ${entityType}: ${response.status()}
- Failed to delete ${entityType}: ${response.status()}
AI-assisted analysis of TryGhost/Ghost@47d8b0e2ad (2026-08-13).
Data as JSON: /api/errors/a568a8ad3fa97dc5.
Report an issue: GitHub.