TryGhost/Ghost · error · Error

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

Error message

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

What it means

Thrown by ApiPersistenceAdapter.insert() when the POST to the configured endpoint returns a non-ok response. This is the test-data-factory's generic create-failure signal: it includes the HTTP status and, if the body parsed as JSON, the stringified error body. It is the e2e equivalent of a server-side create rejection — the Admin API refused to create the entity.

Source

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

        this.transformResponse = options.transformResponse || ((response: TResponse) => response as unknown);
    }

    protected buildUrl(path?: string): string {
        const url = path ? `${this.endpoint}/${path}` : this.endpoint;
        const params = new URLSearchParams(this.queryParams);
        const queryString = params.toString();
        return queryString ? `${url}?${queryString}` : url;
    }

    async insert<T>(entityType: string, data: T): Promise<T> {
        const response = await this.httpClient.post(this.buildUrl(), {
            data: this.transformRequest(data as unknown as TRequest)
        });

        if (!response.ok()) {
            const errorBody = await response.json().catch(() => null);
            const errorMessage = errorBody ? JSON.stringify(errorBody) : '';
            throw new Error(`Failed to create ${entityType}: ${response.status()} ${errorMessage}`);
        }

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

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

        if (response.status() === 404) {
            throw new Error(`${entityType} with id ${id} not found`);
        }

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

        const body = await response.json() as TResponse;

View on GitHub (pinned to 47d8b0e2ad)

Solutions

  1. Read the embedded status + JSON body in the message — it names the validation failure (e.g. 422 with the field at fault).
  2. Ensure the factory was created from an authenticated HTTP client (e.g. createPostFactory(page.request) where page is the authenticated fixture).
  3. Confirm 'pnpm dev' is running and the admin dev server is reachable at http://127.0.0.1:5174 (per e2e AGENTS.md).
  4. Verify the endpoint and query params passed to ApiPersistenceAdapter match the live Admin API route.
  5. Run 'pnpm build' after factory changes and 'pnpm test:types' to catch contract drift.

Example fix

// before: unauthenticated request client → 401
const factory = new PostFactory(new ApiPersistenceAdapter({
    httpClient: request, // bare Playwright request, no session cookie
    endpoint: `${baseURL}/api/admin/posts/`
}));
await factory.create({title: 'X'}); // throws Failed to create posts: 401 ...

// after: use the authenticated page fixture's request
const factory = createPostFactory(page.request); // carries the session cookie
await factory.create({title: 'X'});
Defensive patterns

Strategy: try-catch

Validate before calling

// Verify the HTTP client is authenticated and the endpoint is reachable before bulk-creating
async function clientCanPost(httpClient, endpoint) {
    const res = await httpClient.get(endpoint);
    return res.ok() || res.status() === 404; // 404 on the collection root is often fine
}

if (await clientCanPost(httpClient, endpoint)) {
    await factory.createMany(list);
}

Try / catch

try {
    return await factory.create(options);
} catch (err) {
    if (/Failed to create .*: 401|403/.test(err.message)) {
        throw new Error('Factory HTTP client is not authenticated. Create it from the authenticated page fixture.');
    }
    throw err;
}

Prevention

When it happens

Trigger: A factory.create() call POSTs to e.g. /api/admin/posts/ and the server returns 4xx/5xx. Frequent causes in the e2e suite: the test's HTTP client isn't authenticated (cookies not propagated from the Playwright BrowserContext), the endpoint is wrong, a required field is missing or fails validation, or the Ghost dev server isn't running.

Common situations: Forgot to create the factory from an authenticated page.request (auth lives on the BrowserContext); baseURL not set so the endpoint resolves wrong; factory builds an entity violating a uniqueness constraint (e.g. duplicate slug); Ghost dev server down or still booting; field names changed after a Ghost API refactor but the factory wasn't updated.

Related errors


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