{"record":{"id":"c9c7ce207f5f0fe3","repo":"XTLS/Xray-core","slug":"connection-ends-c9c7ce","errorCode":null,"errorMessage":"connection ends","messagePattern":"connection ends","errorType":"exception","errorClass":"errors.Error","httpStatus":null,"severity":"warning","filePath":"proxy/freedom/freedom.go","lineNumber":459,"sourceCode":"\t\t}\n\t\tvar reader buf.Reader\n\t\tif destination.Network == net.Network_TCP {\n\t\t\treader = buf.NewReader(conn)\n\t\t} else {\n\t\t\treader = NewPacketReader(conn, h, defaultRule, UDPOverride, destination)\n\t\t}\n\t\tif err := buf.Copy(reader, output, buf.UpdateActivity(timer)); err != nil {\n\t\t\treturn errors.New(\"failed to process response\").Base(err)\n\t\t}\n\t\treturn nil\n\t}\n\n\tif newCtx != nil {\n\t\tctx = newCtx\n\t}\n\n\tif err := task.Run(ctx, requestDone, task.OnSuccess(responseDone, task.Close(output))); err != nil {\n\t\treturn errors.New(\"connection ends\").Base(err)\n\t}\n\n\treturn nil\n}\n\nfunc NewPacketReader(conn net.Conn, h *Handler, defaultRule *FinalRule, UDPOverride net.Destination, DialDest net.Destination) buf.Reader {\n\tiConn := conn\n\tstatConn, ok := iConn.(*stat.CounterConnection)\n\tif ok {\n\t\tiConn = statConn.Connection\n\t}\n\tvar counter stats.Counter\n\tif statConn != nil {\n\t\tcounter = statConn.ReadCounter\n\t}\n\tif c, ok := iConn.(*internet.PacketConnWrapper); ok {\n\t\tisOverridden := false\n\t\tif UDPOverride.Address != nil || UDPOverride.Port != 0 {","sourceCodeStart":441,"sourceCodeEnd":477,"githubUrl":"https://github.com/XTLS/Xray-core/blob/7d214f8b094f75322fa3990f8aadad1c912f24f5/proxy/freedom/freedom.go#L441-L477","documentation":"Terminal wrapper around any error from the requestDone/responseDone task group: task.Run collects the first failure and freedom reports it as \"connection ends\". It is the outermost error a caller of Process sees, so the real cause (\"failed to process request/response\" or a task-level timeout/cancel) is one level down in the error chain.","triggerScenarios":"Either uplink or downlink copy fails, the context is cancelled (client disconnect propagates), or a policy timer aborts the task group. Essentially every non-dial failure of a freedom connection funnels here.","commonSituations":"Normal connection teardown being logged as an error; context cancellation when users close browsers/apps; timeouts as in 566/567.","solutions":["Unwrap with errors.Unwrap/errors.Is to find the embedded cause before acting","io.EOF / context.Canceled / io.ErrClosedPipe underneath are benign — route them to info-level logging instead of alerting","If the cause is a timeout, adjust policy timeouts as in the process-request/response errors","Do not retry blindly; retry only when the base cause is a network error, not EOF/cancel"],"exampleFix":"```go\nerr := freedomHandler.Process(ctx, link, dialer)\nif err != nil {\n  if errors.Is(err, io.EOF) || errors.Is(err, context.Canceled) {\n    // normal teardown\n  }\n}\n```","handlingStrategy":"try-catch","validationCode":null,"typeGuard":"```go\nfunc isBenignTeardown(err error) bool {\n    return errors.Is(err, io.EOF) ||\n        errors.Is(err, context.Canceled) ||\n        errors.Is(err, io.ErrClosedPipe)\n}\n```","tryCatchPattern":"```go\nerr := h.Process(ctx, link, dialer)\nif err != nil && !isBenignTeardown(err) {\n    cause := errors.Unwrap(err) // real failure lives here\n    log.Error().Err(cause).Msg(\"freedom connection failed\")\n}\n```","preventionTips":["Always unwrap before alerting","Classify EOF/cancel as info-level","Map child errors (566/567) to their own remedies"],"tags":["freedom","lifecycle","wrapper","xray"],"backgroundTag":null,"analyzedSha":"7d214f8b094f75322fa3990f8aadad1c912f24f5","analyzedAt":"2026-08-15T14:26:24.325Z","schemaVersion":2},"datasetVersion":"2026-08-15T22:17:37.221Z"}