{"record":{"id":"5b317a3410540563","repo":"fatedier/frp","slug":"natholerespmsg-get-empty-candidate-addresses","errorCode":null,"errorMessage":"natHoleRespMsg get empty candidate addresses","messagePattern":"natHoleRespMsg get empty candidate addresses","errorType":"exception","errorClass":null,"httpStatus":null,"severity":"error","filePath":"pkg/nathole/nathole.go","lineNumber":186,"sourceCode":"\ttimeoutCtx, cancel := context.WithTimeout(ctx, timeout)\n\tdefer cancel()\n\n\tvar natHoleRespMsg *msg.NatHoleResp\n\tm, err := transporter.Do(timeoutCtx, m, laneKey, msg.TypeNameNatHoleResp)\n\tif err != nil {\n\t\treturn nil, fmt.Errorf(\"get natHoleRespMsg error: %v\", err)\n\t}\n\tmm, ok := m.(*msg.NatHoleResp)\n\tif !ok {\n\t\treturn nil, fmt.Errorf(\"get natHoleRespMsg error: invalid message type\")\n\t}\n\tnatHoleRespMsg = mm\n\n\tif natHoleRespMsg.Error != \"\" {\n\t\treturn nil, fmt.Errorf(\"natHoleRespMsg get error info: %s\", natHoleRespMsg.Error)\n\t}\n\tif len(natHoleRespMsg.CandidateAddrs) == 0 {\n\t\treturn nil, fmt.Errorf(\"natHoleRespMsg get empty candidate addresses\")\n\t}\n\treturn natHoleRespMsg, nil\n}\n\n// MakeHole is used to make a NAT hole between client and visitor.\nfunc MakeHole(ctx context.Context, listenConn *net.UDPConn, m *msg.NatHoleResp, key []byte) (*net.UDPConn, *net.UDPAddr, error) {\n\txl := xlog.FromContextSafe(ctx)\n\ttransactionID := NewTransactionID()\n\tsendToRangePortsFunc := func(conn *net.UDPConn, addr string) error {\n\t\treturn sendSidMessage(ctx, conn, m.Sid, transactionID, addr, key, m.DetectBehavior.TTL)\n\t}\n\n\tlistenConns := []*net.UDPConn{listenConn}\n\tvar detectAddrs []string\n\tif m.DetectBehavior.Role == DetectRoleSender {\n\t\t// sender\n\t\tif m.DetectBehavior.SendDelayMs > 0 {\n\t\t\ttime.Sleep(time.Duration(m.DetectBehavior.SendDelayMs) * time.Millisecond)","sourceCodeStart":168,"sourceCodeEnd":204,"githubUrl":"https://github.com/fatedier/frp/blob/6c8a8d0a97d03b44e9528d30b30c70cb9d61b405/pkg/nathole/nathole.go#L168-L204","documentation":"The NatHoleResp from frps is otherwise valid but contains an empty CandidateAddrs list, so the client has no addresses to send detection packets to and hole punching cannot proceed. The server is expected to compute the counterpart's mapped public address candidates from both peers' Prepare messages; an empty list means that computation produced nothing.","triggerScenarios":"ExchangeInfo gets NatHoleResp with zero CandidateAddrs — server analysis failed to derive any candidate address, or a server bug/version skew serialized an empty array.","commonSituations":"Server-side NAT analysis found no usable mapped address (e.g. malformed Prepare from the peer); frps/frpc version mismatch changing how candidate addresses are populated; older frps that does not fill the field.","solutions":["Check frps logs for the analysis of the same transaction ID to see why no candidates were generated","Upgrade both frps and frpc to matching versions so CandidateAddrs is always populated on success","Retry the connection — if the server returns an explicit analysis error instead, fix that first (error 302 path)"],"exampleFix":null,"handlingStrategy":"try-catch","validationCode":null,"typeGuard":null,"tryCatchPattern":"resp, err := nathole.ExchangeInfo(ctx, tp, laneKey, m, timeout)\nif err != nil {\n    return err\n}\nif len(resp.CandidateAddrs) == 0 {\n    return errors.New(\"no candidates; retry or fall back to relay\")\n}","preventionTips":["Run matching frps/frpc versions so server analysis always fills CandidateAddrs","Log the transaction ID alongside this error to correlate with frps analysis logs"],"tags":["nat","p2p","server-response","go"],"backgroundTag":null,"analyzedSha":"6c8a8d0a97d03b44e9528d30b30c70cb9d61b405","analyzedAt":"2026-08-15T06:53:27.215Z","schemaVersion":2},"datasetVersion":"2026-08-15T22:17:37.221Z"}