{"record":{"id":"f817eb2a63c08734","repo":"fatedier/frp","slug":"exec-command-cannot-be-empty","errorCode":null,"errorMessage":"exec command cannot be empty","messagePattern":"exec command cannot be empty","errorType":"validation","errorClass":null,"httpStatus":null,"severity":"error","filePath":"pkg/config/v1/value_source.go","lineNumber":123,"sourceCode":"\t}\n\n\tcontent, err := os.ReadFile(f.Path)\n\tif err != nil {\n\t\treturn \"\", fmt.Errorf(\"failed to read file %s: %v\", f.Path, err)\n\t}\n\n\t// Trim whitespace, which is important for file-based tokens\n\treturn strings.TrimSpace(string(content)), nil\n}\n\n// Validate validates the ExecSource configuration.\nfunc (e *ExecSource) Validate() error {\n\tif e == nil {\n\t\treturn errors.New(\"execSource cannot be nil\")\n\t}\n\n\tif e.Command == \"\" {\n\t\treturn errors.New(\"exec command cannot be empty\")\n\t}\n\n\tfor _, env := range e.Env {\n\t\tif env.Name == \"\" {\n\t\t\treturn errors.New(\"exec env name cannot be empty\")\n\t\t}\n\t\tif strings.Contains(env.Name, \"=\") {\n\t\t\treturn errors.New(\"exec env name cannot contain '='\")\n\t\t}\n\t}\n\treturn nil\n}\n\n// Resolve reads and returns the content captured from stdout of launched subprocess.\nfunc (e *ExecSource) Resolve(ctx context.Context) (string, error) {\n\tif err := e.Validate(); err != nil {\n\t\treturn \"\", err\n\t}","sourceCodeStart":105,"sourceCodeEnd":141,"githubUrl":"https://github.com/fatedier/frp/blob/6c8a8d0a97d03b44e9528d30b30c70cb9d61b405/pkg/config/v1/value_source.go#L105-L141","documentation":"Thrown by ExecSource.Validate() when a ValueSource of type \"exec\" has an empty Command. ExecSource.Resolve launches the command via exec.CommandContext and captures stdout, so a command is mandatory; this check fires before validation of args or env entries.","triggerScenarios":"tokenSource.exec table present but command key missing or empty; Go literal v1.ExecSource{Args: []string{\"--flag\"}} with Command unset; YAML indentation mistake putting command under the wrong table so it is not decoded.","commonSituations":"Commenting out the command while testing; config templates that inject the command from an environment variable that is unset; switching from file to exec token source and only copying args/env.","solutions":["Set tokenSource.exec.command to an executable that prints the token to stdout","Check TOML/YAML indentation: command must be a key inside [auth.tokenSource.exec]","Confirm the command is on PATH or use an absolute path","Validate with frpc verify -c ./frpc.toml before restart"],"exampleFix":"# before\n[auth.tokenSource.exec]\nargs = [\"--json\"]\n\n# after\n[auth.tokenSource.exec]\ncommand = \"cloudctl\"\nargs = [\"--json\", \"token\", \"print\"]","handlingStrategy":"validation","validationCode":"if vs := cfg.Auth.TokenSource; vs != nil && vs.Type == \"exec\" && vs.Exec != nil {\n    if vs.Exec.Command == \"\" {\n        return fmt.Errorf(\"tokenSource.exec.command is empty\")\n    }\n}","typeGuard":"func hasCommand(e *v1.ExecSource) bool {\n    return e != nil && strings.TrimSpace(e.Command) != \"\"\n}","tryCatchPattern":null,"preventionTips":["Smoke-test the token command manually (it must print the token to stdout) before configuring it","Use absolute paths for the command to avoid PATH differences under systemd/containers"],"tags":["frp","go","config","validation","exec"],"backgroundTag":null,"analyzedSha":"6c8a8d0a97d03b44e9528d30b30c70cb9d61b405","analyzedAt":"2026-08-15T06:53:27.215Z","schemaVersion":2},"datasetVersion":"2026-08-15T17:31:12.345Z"}