{"record":{"id":"b600da4c027c44aa","repo":"pocketbase/pocketbase","slug":"invalid-sort-field-q","errorCode":null,"errorMessage":"invalid sort field %q","messagePattern":"invalid sort field %q","errorType":"validation","errorClass":null,"httpStatus":null,"severity":"error","filePath":"tools/search/sort.go","lineNumber":41,"sourceCode":"}\n\n// BuildExpr resolves the sort field into a valid db sort expression.\nfunc (s *SortField) BuildExpr(fieldResolver FieldResolver) (string, error) {\n\t// special case for random sort\n\tif s.Name == randomSortKey {\n\t\treturn \"RANDOM()\", nil\n\t}\n\n\t// special case for the builtin SQLite rowid column\n\tif s.Name == rowidSortKey {\n\t\treturn fmt.Sprintf(\"[[_rowid_]] %s\", s.Direction), nil\n\t}\n\n\tresult, err := fieldResolver.Resolve(s.Name)\n\n\t// invalidate empty fields and non-column identifiers\n\tif err != nil || len(result.Params) > 0 || result.Identifier == \"\" || strings.ToLower(result.Identifier) == \"null\" {\n\t\treturn \"\", fmt.Errorf(\"invalid sort field %q\", s.Name)\n\t}\n\n\treturn fmt.Sprintf(\"%s %s\", result.Identifier, s.Direction), nil\n}\n\n// ParseSortFromString parses the provided string expression\n// into a slice of SortFields.\n//\n// Example:\n//\n//\tfields := search.ParseSortFromString(\"-name,+created\")\nfunc ParseSortFromString(str string) (fields []SortField) {\n\tdata := strings.Split(str, \",\")\n\n\tfor _, field := range data {\n\t\t// trim whitespaces\n\t\tfield = strings.TrimSpace(field)\n\t\tif strings.HasPrefix(field, \"-\") {","sourceCodeStart":23,"sourceCodeEnd":59,"githubUrl":"https://github.com/pocketbase/pocketbase/blob/5d217ddb50cb144d80a5d0b0bdf11b52b2c3e457/tools/search/sort.go#L23-L59","documentation":"Produced when building an ORDER BY expression: the sort field either fails to resolve via the field resolver, resolves to something with bound parameters, resolves to an empty identifier, or resolves to the literal `NULL`. Only plain column identifiers are sortable, so any non-column result (e.g. a macro expansion or a parameterized value) is rejected as an invalid sort field.","triggerScenarios":"Sorting by a field not in the resolver's allowlist (e.g. `sort=secretField`); sorting by `@now` or another macro (resolves to a bound parameter); sorting by an identifier that resolves to empty/`NULL`; using `sort=` with a mistyped column name.","commonSituations":"API requests with `sort=` parameters naming non-allowlisted or computed fields; renaming schema fields while client code still sorts by the old name; trying to sort by JSON path or relation fields not permitted by the resolver.","solutions":["Sort only by real, allowlisted column fields","Fix typos/case in the sort field name","Add the intended sort field to the field resolver's allowed fields if sorting on it is legitimate","Remove macros or expressions from the sort parameter — they are only valid in filters"],"exampleFix":"// before\nsort := \"@now\" // or \"nonExistentField\"\n// after\nsort := \"created\"","handlingStrategy":"validation","validationCode":"// validate sort fields before building the query\nfor _, sf := range search.ParseSortFromString(sortParam) {\n    if _, err := resolver.Resolve(sf.Name); err != nil {\n        return fmt.Errorf(\"invalid sort field %q\", sf.Name)\n    }\n}","typeGuard":"func isSortableField(name string, resolver search.FieldResolver) bool {\n    r, err := resolver.Resolve(name)\n    return err == nil && r.Identifier != \"\" && len(r.Params) == 0 && strings.ToLower(r.Identifier) != \"null\"\n}","tryCatchPattern":"expr, err := search.ParseSort(sortParam).BuildExpr(resolver)\nif err != nil {\n    if strings.Contains(err.Error(), \"invalid sort field\") {\n        // drop bad sort fields and retry with a safe default like \"id\"\n    }\n}","preventionTips":["Expose only allowlisted fields as sortable in API contracts","Strip macros and expressions from sort parameters at the API boundary","Return 400 with the offending field name instead of a 500 when sort resolution fails","Keep client sort lists synchronized with schema renames"],"tags":["pocketbase","sort","resolver","search","sql"],"backgroundTag":null,"analyzedSha":"5d217ddb50cb144d80a5d0b0bdf11b52b2c3e457","analyzedAt":"2026-08-15T10:06:33.165Z","schemaVersion":2},"datasetVersion":"2026-08-15T17:31:12.345Z"}