{"record":{"id":"dc33aba207bffcb0","repo":"XTLS/Xray-core","slug":"outbound-failed-to-relay-connection","errorCode":null,"errorMessage":"outbound failed to relay connection","messagePattern":"outbound failed to relay connection","errorType":"console","errorClass":"errors.Error","httpStatus":null,"severity":"warning","filePath":"app/observatory/observer.go","lineNumber":179,"sourceCode":"\t\tTransport: &httpTransport,\n\t\tCheckRedirect: func(req *http.Request, via []*http.Request) error {\n\t\t\treturn http.ErrUseLastResponse\n\t\t},\n\t\tJar:     nil,\n\t\tTimeout: time.Second * 5,\n\t}\n\tvar GETTime time.Duration\n\terr := task.Run(o.ctx, func() error {\n\t\tstartTime := time.Now()\n\t\tprobeURL := \"https://www.google.com/generate_204\"\n\t\tif o.config.ProbeUrl != \"\" {\n\t\t\tprobeURL = o.config.ProbeUrl\n\t\t}\n\t\treq, _ := http.NewRequest(http.MethodGet, probeURL, nil)\n\t\tutils.TryDefaultHeadersWith(req.Header, \"nav\")\n\t\tresponse, err := httpClient.Do(req)\n\t\tif err != nil {\n\t\t\treturn errors.New(\"outbound failed to relay connection\").Base(err)\n\t\t}\n\t\tif response.Body != nil {\n\t\t\tresponse.Body.Close()\n\t\t}\n\t\tendTime := time.Now()\n\t\tGETTime = endTime.Sub(startTime)\n\t\treturn nil\n\t})\n\tif err != nil {\n\t\terrorMessage := \"the outbound \" + outbound + \" is dead: GET request failed:\" + err.Error() + \"with outbound handler report underlying connection failed\"\n\t\terrors.LogInfoInner(o.ctx, errorCollectorForRequest.UnderlyingError(), errorMessage)\n\t\treturn ProbeResult{Alive: false, LastErrorReason: errorMessage}\n\t}\n\terrors.LogInfo(o.ctx, \"the outbound \", outbound, \" is alive:\", GETTime.Seconds())\n\treturn ProbeResult{Alive: true, Delay: GETTime.Milliseconds()}\n}\n\nfunc (o *Observer) updateStatusForResult(outbound string, result *ProbeResult) {","sourceCodeStart":161,"sourceCodeEnd":197,"githubUrl":"https://github.com/XTLS/Xray-core/blob/7d214f8b094f75322fa3990f8aadad1c912f24f5/app/observatory/observer.go#L161-L197","documentation":"Thrown in app/observatory/observer.go:179 when the probe HTTP GET (default https://www.google.com/generate_204, or config.ProbeUrl) returns an error from httpClient.Do. Unlike the dialer errors, this covers the whole request lifecycle — dial, TLS handshake (5s timeout), sending, and response — and marks the outbound as failed for this observation round.","triggerScenarios":"httpClient.Do fails for any reason: the dialer path failed (see 45-47), TLS handshake exceeded 5 seconds, the 5-second overall client Timeout was exceeded, or the connection was reset mid-request. The observer then records ProbeResult{Alive:false} with the composed 'outbound X is dead' message.","commonSituations":"Classic in geo-restricted or censored networks where google.com is unreachable directly through the outbound; also common with slow outbounds that cannot complete a full TLS+HTTP round trip in 5 seconds, causing the observer to flap an actually-working node.","solutions":["Set observatory config ProbeUrl to an endpoint reachable through your outbounds (e.g. https://www.gstatic.com/generate_204 or a self-hosted 204 URL).","If the outbound is legitimately slow, accept occasional dead marks or raise health-check tolerance via burst observatory with tuned interval.","Verify the outbound credentials/streamSettings if probes consistently fail while manual traffic also fails.","Read the chained Base error and errorCollector.UnderlyingError() to distinguish dial vs TLS vs timeout."],"exampleFix":"// before\n\"observatory\": {\"subjectSelector\": [\"proxy\"]}\n\n// after\n\"observatory\": {\n  \"subjectSelector\": [\"proxy\"],\n  \"probeURL\": \"https://www.gstatic.com/generate_204\",\n  \"probeInterval\": \"1m\"\n}","handlingStrategy":"retry","validationCode":"// pre-flight: confirm the probe URL is reachable through the outbound before scheduling probes\nresp, err := probeClient.Get(probeURL)\nif err != nil { chooseDifferentProbeURL() }","typeGuard":null,"tryCatchPattern":"// observatory already retries per probeInterval; consumers should tolerate transient dead marks\nif !result.Alive && result.AlivePrev { /* flap: delay removal by one more interval */ }","preventionTips":["Point ProbeUrl at a highly available 204 endpoint.","Do not treat a single failed probe as permanent node death; require consecutive failures.","Keep probe timeouts (5s hardcoded) in mind for slow nodes."],"tags":["go","xray","observatory","health-check","network"],"backgroundTag":null,"analyzedSha":"7d214f8b094f75322fa3990f8aadad1c912f24f5","analyzedAt":"2026-08-15T14:26:24.325Z","schemaVersion":2},"datasetVersion":"2026-08-15T17:31:12.345Z"}