{"record":{"id":"63cfb02eb21b44fe","repo":"redis/go-redis","slug":"redis-ft-aggregate-collect-empty-field-name-in-f","errorCode":null,"errorMessage":"redis: FT.AGGREGATE COLLECT: empty field name in Fields","messagePattern":"redis: FT\\.AGGREGATE COLLECT: empty field name in Fields","errorType":"validation","errorClass":null,"httpStatus":null,"severity":"error","filePath":"search_collect.go","lineNumber":94,"sourceCode":"\treturn \"@\" + strings.TrimLeft(name, \"@\")\n}\n\n// buildCollectArgs renders a FTAggregateCollect into the reducer argument\n// token list (everything after \"REDUCE COLLECT <narg>\", excluding AS <alias>).\n// The serializer computes <narg> as len(args), which matches the COLLECT\n// contract: narg counts every FIELDS/DISTINCT/SORTBY/LIMIT token.\nfunc buildCollectArgs(o FTAggregateCollect) ([]interface{}, error) {\n\targs := make([]interface{}, 0, 8)\n\n\t// FIELDS (required): either * or a counted list of @-names.\n\tswitch {\n\tcase o.FieldsAll:\n\t\targs = append(args, \"FIELDS\", \"*\")\n\tcase len(o.Fields) > 0:\n\t\targs = append(args, \"FIELDS\", len(o.Fields))\n\t\tfor _, f := range o.Fields {\n\t\t\tif strings.TrimLeft(f, \"@\") == \"\" {\n\t\t\t\treturn nil, fmt.Errorf(\"redis: FT.AGGREGATE COLLECT: empty field name in Fields\")\n\t\t\t}\n\t\t\targs = append(args, ensureAtPrefix(f))\n\t\t}\n\tdefault:\n\t\treturn nil, fmt.Errorf(\"redis: FT.AGGREGATE COLLECT requires FieldsAll or a non-empty Fields list\")\n\t}\n\n\t// DISTINCT (optional, forward-compatible).\n\tif o.Distinct {\n\t\targs = append(args, \"DISTINCT\")\n\t}\n\n\t// SORTBY (optional). sort_narg counts each field plus its optional\n\t// direction token.\n\tif len(o.SortBy) > 0 {\n\t\tsortTokens := make([]interface{}, 0, len(o.SortBy)*2)\n\t\tfor _, s := range o.SortBy {\n\t\t\tif strings.TrimLeft(s.FieldName, \"@\") == \"\" {","sourceCodeStart":76,"sourceCodeEnd":112,"githubUrl":"https://github.com/redis/go-redis/blob/c5cad058c72f58370553b48566302303cf8a2e89/search_collect.go#L76-L112","documentation":"The COLLECT aggregation step's FIELDS list must contain non-empty field names. buildCollectArgs validates each entry, stripping a leading '@'; an entry that is empty or only '@' names no attribute, so argument building aborts with this error before the command is sent.","triggerScenarios":"NewCollectReducer with CollectOptions.Fields containing \"\" or \"@\"; buildCollectArgs hits strings.TrimLeft(f, \"@\") == \"\" and returns nil plus this error.","commonSituations":"Building COLLECT field lists programmatically from user input or config where a field was left blank; a stray '@' typo instead of a field name like '@name'.","solutions":["Replace empty/'@'-only entries in Fields with actual field names (with or without the '@' prefix)","Filter out empty strings before constructing the Collect reducer","Validate the field list at config-load time"],"exampleFix":"// before\nr := redis.NewCollectReducer(redis.CollectOptions{Fields: []string{\"@\", \"@age\"}})\n// after\nr := redis.NewCollectReducer(redis.CollectOptions{Fields: []string{\"@name\", \"@age\"}})","handlingStrategy":"validation","validationCode":"for _, f := range fields {\n    if strings.TrimLeft(f, \"@\") == \"\" {\n        return fmt.Errorf(\"invalid COLLECT field: %q\", f)\n    }\n}","typeGuard":null,"tryCatchPattern":null,"preventionTips":["Never pass user-supplied field names without trimming and emptiness checks","Use FieldsAll instead of a placeholder Fields list when all fields are wanted","Sanitize config-driven field lists at startup"],"tags":["search","ft-aggregate","collect","validation"],"backgroundTag":"empty-required-field","analyzedSha":"c5cad058c72f58370553b48566302303cf8a2e89","analyzedAt":"2026-09-01T06:50:53.388Z","contentChangedAt":null,"schemaVersion":2},"datasetVersion":"2026-09-08T10:18:20.063Z"}