{"record":{"id":"5229ed18b1262780","repo":"fatedier/frp","slug":"do-http-health-check-statuscode-is-d-not-2xx","errorCode":null,"errorMessage":"do http health check, StatusCode is [%d] not 2xx","messagePattern":"do http health check, StatusCode is \\[(.+?)\\] not 2xx","errorType":"exception","errorClass":null,"httpStatus":null,"severity":"warning","filePath":"client/health/health.go","lineNumber":182,"sourceCode":"\treturn nil\n}\n\nfunc (monitor *Monitor) doHTTPCheck(ctx context.Context) error {\n\treq, err := http.NewRequestWithContext(ctx, \"GET\", monitor.url, nil)\n\tif err != nil {\n\t\treturn err\n\t}\n\treq.Header = monitor.header\n\treq.Host = monitor.header.Get(\"Host\")\n\tresp, err := http.DefaultClient.Do(req)\n\tif err != nil {\n\t\treturn err\n\t}\n\tdefer resp.Body.Close()\n\t_, _ = io.Copy(io.Discard, resp.Body)\n\n\tif resp.StatusCode/100 != 2 {\n\t\treturn fmt.Errorf(\"do http health check, StatusCode is [%d] not 2xx\", resp.StatusCode)\n\t}\n\treturn nil\n}\n","sourceCodeStart":164,"sourceCodeEnd":186,"githubUrl":"https://github.com/fatedier/frp/blob/6c8a8d0a97d03b44e9528d30b30c70cb9d61b405/client/health/health.go#L164-L186","documentation":"Thrown by frpc's HTTP health monitor (Monitor.doHTTPCheck) when the proxy's health.check.httpPath endpoint answers with a status outside the 200-299 range. The monitor issues a GET (with the configured headers, including Host) to the local service; any non-2xx reply is treated as 'unhealthy' and returned as this error. It only fires when healthCheck.type is \"http\"; TCP monitors dial instead and never produce it.","triggerScenarios":"A proxy config with transport.healthCheck.type = \"http\" and healthCheck.path (or URL) pointing at an endpoint that returns 301/404/500/etc.; e.g. the service is a plain TCP server that answers with garbage (parsed as 5xx), or the path is wrong so the app returns 404.","commonSituations":"Wrong healthCheck.path for the app's routes; app redirects HTTP to HTTPS (301); app returns 401 on unauthenticated health probes; pointing the HTTP check at a non-HTTP local service.","solutions":["Fix healthCheck.path/URL to hit an endpoint that actually returns 200 (e.g. /healthz or /).","If the local service is not HTTP, change healthCheck.type to \"tcp\" so only connectability is checked.","If the app redirects (301/302) or requires a header, set healthCheck.headers (e.g. Host, Authorization) or make the endpoint respond 200 directly.","If the app itself is erroring (500), fix or start the local application; frpc is correctly reporting it as down."],"exampleFix":"# before (frpc.toml)\n[[proxies]]\nname = \"web\"\ntype = \"http\"\n[proxies.healthCheck]\ntype = \"http\"\npath = \"/\"\n\n# after\n[proxies.healthCheck]\ntype = \"http\"\npath = \"/healthz\"\nheaders = { Host = \"myapp.local\" }","handlingStrategy":"retry","validationCode":"// Verify the health endpoint returns 2xx before enabling the http health check\nresp, err := http.Get(\"http://\" + net.JoinHostPort(localIP, localPort) + healthPath)\nif err != nil { return err }\nif resp.StatusCode/100 != 2 { return fmt.Errorf(\"endpoint returns %d, fix app or use tcp check\", resp.StatusCode) }","typeGuard":null,"tryCatchPattern":"// In custom monitors wrapping frpc health: treat non-2xx as unhealthy, log and retry after interval\nif err := monitor.Check(ctx); err != nil {\n    if strings.Contains(err.Error(), \"not 2xx\") { log.Warnf(\"app unhealthy: %v\", err); time.Sleep(interval); continue }\n    return err\n}","preventionTips":["Point healthCheck.path at a real 200 endpoint (/healthz) before enabling type=http checks.","Use type=tcp health checks for non-HTTP local services.","Make health endpoints skip auth/redirects so probes get plain 200s."],"tags":["http","health-check","frpc","monitoring"],"backgroundTag":null,"analyzedSha":"6c8a8d0a97d03b44e9528d30b30c70cb9d61b405","analyzedAt":"2026-08-15T06:53:27.215Z","schemaVersion":2},"datasetVersion":"2026-08-15T22:17:37.221Z"}