fatedier/frp · warning
do http health check, StatusCode is [%d] not 2xx
Error message
do http health check, StatusCode is [%d] not 2xx
What it means
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.
Source
Thrown at client/health/health.go:182
return nil
}
func (monitor *Monitor) doHTTPCheck(ctx context.Context) error {
req, err := http.NewRequestWithContext(ctx, "GET", monitor.url, nil)
if err != nil {
return err
}
req.Header = monitor.header
req.Host = monitor.header.Get("Host")
resp, err := http.DefaultClient.Do(req)
if err != nil {
return err
}
defer resp.Body.Close()
_, _ = io.Copy(io.Discard, resp.Body)
if resp.StatusCode/100 != 2 {
return fmt.Errorf("do http health check, StatusCode is [%d] not 2xx", resp.StatusCode)
}
return nil
}
View on GitHub (pinned to 6c8a8d0a97)
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.
Example fix
# before (frpc.toml)
[[proxies]]
name = "web"
type = "http"
[proxies.healthCheck]
type = "http"
path = "/"
# after
[proxies.healthCheck]
type = "http"
path = "/healthz"
headers = { Host = "myapp.local" } Defensive patterns
Strategy: retry
Validate before calling
// Verify the health endpoint returns 2xx before enabling the http health check
resp, err := http.Get("http://" + net.JoinHostPort(localIP, localPort) + healthPath)
if err != nil { return err }
if resp.StatusCode/100 != 2 { return fmt.Errorf("endpoint returns %d, fix app or use tcp check", resp.StatusCode) } Try / catch
// In custom monitors wrapping frpc health: treat non-2xx as unhealthy, log and retry after interval
if err := monitor.Check(ctx); err != nil {
if strings.Contains(err.Error(), "not 2xx") { log.Warnf("app unhealthy: %v", err); time.Sleep(interval); continue }
return err
} Prevention
- 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.
When it happens
Trigger: 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.
Common situations: 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.
Related errors
- HTTP ${response.status}
- ErrHealthCheckType
- subdomain and custom domains should not be both empty
- invalid argument: body can't be empty
- HTTP ${response.status}
AI-assisted analysis of fatedier/frp@6c8a8d0a97 (2026-08-15).
Data as JSON: /api/errors/5229ed18b1262780.
Report an issue: GitHub.