TryGhost/Ghost · warning · Error
Failed to delete ${entityType}: ${response.status()}
Error message
Failed to delete ${entityType}: ${response.status()} What it means
Thrown by ApiPersistenceAdapter.delete() when the DELETE returns non-ok AND the status is not 404. The adapter deliberately treats 404 as success (idempotent delete — the record is gone either way), so this throw fires only on auth failures (401/403), server errors (500), or conflicts (e.g. the server refuses to delete a referenced entity).
Source
Thrown at e2e/data-factory/persistence/adapters/api.ts:88
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 → grant delete permission to the test role; 409/422 → remove referencing entities first; 500 → Ghost server log.
- Ensure teardown runs with an authenticated admin client.
- If the entity is referenced, delete dependents first or use the cascade the Admin API provides.
- Remember 404 is already tolerated — a repeat delete after removal won't throw this.
- Run cleanup in afterEach/beforeAll hooks with the same auth context used for creation.
Defensive patterns
Strategy: try-catch
Validate before calling
// Idempotent delete helper — 404 is already tolerated by the adapter, but wrap for other transient failures
async function safeDelete(adapter, entityType, id) {
try {
await adapter.delete(entityType, id);
} catch (err) {
if (/Failed to delete .*: (403|409|500)/.test(err.message)) throw err;
// tolerate other statuses in teardown
}
} Try / catch
try {
await factory.delete(id);
} catch (err) {
const status = /Failed to delete .*: (\d+)/.exec(err.message)?.[1];
if (status === '403') console.warn('No delete permission for', id);
else if (status === '409' || status === '422') console.warn('Entity still referenced, skipping delete', id);
else throw err;
} Prevention
- Run teardown with an admin-authenticated client.
- Delete dependents before parents to avoid business-rule conflicts.
- Remember the adapter already treats 404 as success — repeated cleanup is safe.
When it happens
Trigger: The test role lacks delete permission (403); the server refuses deletion because the entity is still referenced (e.g. deleting a tier that has subscriptions — a 4xx/5xx business rule); the Ghost server errored (500); authentication not propagated.
Common situations: Running cleanup of test data without admin privileges; deleting a parent entity whose children still reference it; the dev server errored mid-delete; the HTTP client lost its session between create and teardown.
Related errors
- Failed to create ${entityType}: ${response.status()} ${error
- ${entityType} with id ${id} not found
- Failed to find ${entityType}: ${response.status()}
- Failed to update ${entityType}: ${response.status()}
- Cannot create without a persistence adapter. Use buildMany()
AI-assisted analysis of TryGhost/Ghost@47d8b0e2ad (2026-08-13).
Data as JSON: /api/errors/86052c1ef65738e8.
Report an issue: GitHub.