{"record":{"id":"737a4fb71aabc059","repo":"TryGhost/Ghost","slug":"the-nql-filter-you-passed-was-invalid","errorCode":null,"errorMessage":"The NQL filter you passed was invalid.","messagePattern":"The NQL filter you passed was invalid\\.","errorType":"http","errorClass":"BadRequestError","httpStatus":400,"severity":"error","filePath":"ghost/core/core/server/api/endpoints/utils/serializers/input/tiers.js","lineNumber":64,"sourceCode":"        converted.monthlyPrice = converted.monthly_price;\n        delete converted.monthly_price;\n    }\n\n    if (Reflect.has(converted, 'yearly_price')) {\n        converted.yearlyPrice = converted.yearly_price;\n        delete converted.yearly_price;\n    }\n\n    return converted;\n}\n\nmodule.exports = {\n    all(_apiConfig, frame) {\n        if (frame.options.filter) {\n            try {\n                frame.options.filter = nql.parse(frame.options.filter);\n            } catch (err) {\n                throw new BadRequestError({\n                    message: tpl(messages.invalidNQLFilter)\n                });\n            }\n        } else {\n            frame.options.filter = null;\n        }\n\n        if (localUtils.isContentAPI(frame)) {\n            // CASE: content api can only have active tiers\n            forceActiveFilter(frame);\n\n            // CASE: content api includes these by default\n            const defaultRelations = ['monthly_price', 'yearly_price', 'benefits'];\n            if (!frame.options.withRelated) {\n                frame.options.withRelated = defaultRelations;\n            } else {\n                for (const relation of defaultRelations) {\n                    if (!frame.options.withRelated.includes(relation)) {","sourceCodeStart":46,"sourceCodeEnd":82,"githubUrl":"https://github.com/TryGhost/Ghost/blob/47d8b0e2ad2fd4757d3bc45f46c3ac165ff8a1fe/ghost/core/core/server/api/endpoints/utils/serializers/input/tiers.js#L46-L82","documentation":"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`.","triggerScenarios":"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.","commonSituations":"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.","solutions":["Validate/escape the filter string client-side before sending the request.","Test the filter against NQL syntax documentation; balance parentheses and quotes.","Use only supported NQL operators and field names for tiers.","Simplify the filter incrementally to find the offending token."],"exampleFix":"// before\nawait api.tiers.browse({filter: \"status:'active\"}); // unbalanced quote\n\n// after\nawait api.tiers.browse({filter: 'status:active'});","handlingStrategy":"validation","validationCode":"import nql from '@tryghost/nql';\nfunction isValidFilter(filter: string): boolean {\n    try { nql.parse(filter); return true; } catch { return false; }\n}","typeGuard":null,"tryCatchPattern":"try {\n    await api.tiers.browse({filter});\n} catch (err) {\n    if (/invalid NQL filter/i.test(JSON.stringify((err as any).response?.body))) {\n        // simplify/escape the filter and retry\n    }\n    throw err;\n}","preventionTips":["Validate NQL filter syntax client-side before sending.","Balance parentheses and quotes in filter expressions.","Use only NQL-supported operators and valid tier field names."],"tags":["api","nql","filters","tiers","validation"],"backgroundTag":null,"analyzedSha":"47d8b0e2ad2fd4757d3bc45f46c3ac165ff8a1fe","analyzedAt":"2026-08-13T01:25:26.651Z","schemaVersion":2},"datasetVersion":"2026-08-13T04:17:16.726Z"}