TryGhost/Ghost · error · BadRequestError

The NQL filter you passed was invalid.

Error message

The NQL filter you passed was invalid.

What it means

Thrown by the tiers serializer `all` method when `nql.parse(frame.options.filter)` raises an exception. Ghost parses the supplied `filter` query string into an NQL AST before querying tiers; invalid syntax is caught and converted to a 400 BadRequestError with `invalidNQLFilter`.

Source

Thrown at ghost/core/core/server/api/endpoints/utils/serializers/input/tiers.js:64

        converted.monthlyPrice = converted.monthly_price;
        delete converted.monthly_price;
    }

    if (Reflect.has(converted, 'yearly_price')) {
        converted.yearlyPrice = converted.yearly_price;
        delete converted.yearly_price;
    }

    return converted;
}

module.exports = {
    all(_apiConfig, frame) {
        if (frame.options.filter) {
            try {
                frame.options.filter = nql.parse(frame.options.filter);
            } catch (err) {
                throw new BadRequestError({
                    message: tpl(messages.invalidNQLFilter)
                });
            }
        } else {
            frame.options.filter = null;
        }

        if (localUtils.isContentAPI(frame)) {
            // CASE: content api can only have active tiers
            forceActiveFilter(frame);

            // CASE: content api includes these by default
            const defaultRelations = ['monthly_price', 'yearly_price', 'benefits'];
            if (!frame.options.withRelated) {
                frame.options.withRelated = defaultRelations;
            } else {
                for (const relation of defaultRelations) {
                    if (!frame.options.withRelated.includes(relation)) {

View on GitHub (pinned to 47d8b0e2ad)

Solutions

  1. Validate/escape the filter string client-side before sending the request.
  2. Test the filter against NQL syntax documentation; balance parentheses and quotes.
  3. Use only supported NQL operators and field names for tiers.
  4. Simplify the filter incrementally to find the offending token.

Example fix

// before
await api.tiers.browse({filter: "status:'active"}); // unbalanced quote

// after
await api.tiers.browse({filter: 'status:active'});
Defensive patterns

Strategy: validation

Validate before calling

import nql from '@tryghost/nql';
function isValidFilter(filter: string): boolean {
    try { nql.parse(filter); return true; } catch { return false; }
}

Try / catch

try {
    await api.tiers.browse({filter});
} catch (err) {
    if (/invalid NQL filter/i.test(JSON.stringify((err as any).response?.body))) {
        // simplify/escape the filter and retry
    }
    throw err;
}

Prevention

When it happens

Trigger: A GET request to the tiers API includes a `?filter=` query with syntactically invalid NQL (unbalanced parentheses, unknown operators, bad field references that the parser rejects), and `nql.parse` throws.

Common situations: Typo in filter syntax (e.g., `status:active'`), mismatched quotes/parentheses, using SQL-style operators not supported by NQL, passing a raw user-provided string unescaped, or a client library generating malformed filters after an upgrade.

Related errors


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