{"record":{"id":"24976bd50a795172","repo":"fatedier/frp","slug":"load-source-w","errorCode":null,"errorMessage":"load source: %w","messagePattern":"load source: %w","errorType":"validation","errorClass":null,"httpStatus":null,"severity":"error","filePath":"pkg/config/source/aggregator.go","lineNumber":85,"sourceCode":"\treturn sources\n}\n\nfunc (a *Aggregator) Load() ([]v1.ProxyConfigurer, []v1.VisitorConfigurer, error) {\n\ta.mu.RLock()\n\tentries := a.getSourcesLocked()\n\ta.mu.RUnlock()\n\n\tif len(entries) == 0 {\n\t\treturn nil, nil, errors.New(\"no sources configured\")\n\t}\n\n\tproxyMap := make(map[string]v1.ProxyConfigurer)\n\tvisitorMap := make(map[string]v1.VisitorConfigurer)\n\n\tfor _, src := range entries {\n\t\tproxies, visitors, err := src.Load()\n\t\tif err != nil {\n\t\t\treturn nil, nil, fmt.Errorf(\"load source: %w\", err)\n\t\t}\n\t\tfor _, p := range proxies {\n\t\t\tproxyMap[p.GetBaseConfig().Name] = p\n\t\t}\n\t\tfor _, v := range visitors {\n\t\t\tvisitorMap[v.GetBaseConfig().Name] = v\n\t\t}\n\t}\n\tproxies, visitors := a.mapsToSortedSlices(proxyMap, visitorMap)\n\treturn proxies, visitors, nil\n}\n\nfunc (a *Aggregator) mapsToSortedSlices(\n\tproxyMap map[string]v1.ProxyConfigurer,\n\tvisitorMap map[string]v1.VisitorConfigurer,\n) ([]v1.ProxyConfigurer, []v1.VisitorConfigurer) {\n\tproxies := slices.SortedFunc(maps.Values(proxyMap), func(x, y v1.ProxyConfigurer) int {\n\t\treturn cmp.Compare(x.GetBaseConfig().Name, y.GetBaseConfig().Name)","sourceCodeStart":67,"sourceCodeEnd":103,"githubUrl":"https://github.com/fatedier/frp/blob/6c8a8d0a97d03b44e9528d30b30c70cb9d61b405/pkg/config/source/aggregator.go#L67-L103","documentation":"The aggregator source iterates every registered config source (file, store, etc.) and calls src.Load(); the first source that fails aborts aggregation with 'load source: %w'. The %w wrapping preserves the underlying error for errors.Is/As inspection, so the real cause (file parse failure, store read failure) stays chainable.","triggerScenarios":"Any configured source failing to load: a file source watching a config that becomes invalid, or a store source whose JSON file is corrupted. Sources load in registration order, so the first failure wins.","commonSituations":"frpc deployments with multiple config sources (static file + dynamic store); editing the live-watched config file into a temporarily invalid state; a corrupted store JSON after disk issues.","solutions":["Inspect the wrapped error chain (errors.Unwrap / string suffix) to find which source and why","Fix that source's underlying file/content","For watched sources, write changes atomically (write temp file + rename) so the watcher never reads a half-written config"],"exampleFix":"# before: echo 'bad toml' >> /etc/frp/frpc.toml while running\n# after: write /etc/frp/frpc.toml.tmp fully, then mv /etc/frp/frpc.toml.tmp /etc/frp/frpc.toml","handlingStrategy":"try-catch","validationCode":"// probe each source before wiring the aggregator\nfor _, src := range sources { if _, _, err := src.Load(); err != nil { return fmt.Errorf(\"source %T unhealthy: %w\", src, err) } }","typeGuard":null,"tryCatchPattern":"if _, _, err := aggregator.Load(); err != nil {\n    var cause error = errors.Unwrap(err) // 'load source: %w' keeps chain\n    for cause != nil && errors.Unwrap(cause) != nil { cause = errors.Unwrap(cause) }\n    // log full chain; identify failing source type\n}","preventionTips":["Use %w-friendly inspection (errors.As/Is) rather than string matching","Write watched config files atomically (tmp+rename)","Keep the store file backed up and JSON-validated"],"tags":["config","aggregator","error-wrapping","sources","frpc"],"backgroundTag":null,"analyzedSha":"6c8a8d0a97d03b44e9528d30b30c70cb9d61b405","analyzedAt":"2026-08-15T06:53:27.215Z","schemaVersion":2},"datasetVersion":"2026-08-15T17:31:12.345Z"}