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

  1. Fix healthCheck.path/URL to hit an endpoint that actually returns 200 (e.g. /healthz or /).
  2. If the local service is not HTTP, change healthCheck.type to "tcp" so only connectability is checked.
  3. If the app redirects (301/302) or requires a header, set healthCheck.headers (e.g. Host, Authorization) or make the endpoint respond 200 directly.
  4. 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

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


AI-assisted analysis of fatedier/frp@6c8a8d0a97 (2026-08-15). Data as JSON: /api/errors/5229ed18b1262780. Report an issue: GitHub.