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

  1. Inspect the status code: 403 → grant delete permission to the test role; 409/422 → remove referencing entities first; 500 → Ghost server log.
  2. Ensure teardown runs with an authenticated admin client.
  3. If the entity is referenced, delete dependents first or use the cascade the Admin API provides.
  4. Remember 404 is already tolerated — a repeat delete after removal won't throw this.
  5. 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

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


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