{"record":{"id":"567abc861c3a6768","repo":"fatedier/frp","slug":"serverhello-error","errorCode":null,"errorMessage":"serverHello.Error","messagePattern":"serverHello\\.Error","errorType":"console","errorClass":null,"httpStatus":null,"severity":"error","filePath":"client/control_session.go","lineNumber":191,"sourceCode":"\t\t_ = conn.SetReadDeadline(time.Time{})\n\t}()\n\n\tvar cryptoContext *wire.CryptoContext\n\tvar udpPacketCodec string\n\tif wireConn != nil {\n\t\tserverHelloFrame, err := wireConn.ReadFrame()\n\t\tif err != nil {\n\t\t\treturn nil, err\n\t\t}\n\t\tif serverHelloFrame.Type != wire.FrameTypeServerHello {\n\t\t\treturn nil, fmt.Errorf(\"unexpected frame type %d, want %d\", serverHelloFrame.Type, wire.FrameTypeServerHello)\n\t\t}\n\t\tvar serverHello wire.ServerHello\n\t\tif err := wireConn.UnmarshalFrame(serverHelloFrame, &serverHello); err != nil {\n\t\t\treturn nil, err\n\t\t}\n\t\tif serverHello.Error != \"\" {\n\t\t\treturn nil, errors.New(serverHello.Error)\n\t\t}\n\t\tcryptoContext, err = wire.NewClientCryptoContext(clientHelloPayload, serverHelloFrame.Payload)\n\t\tif err != nil {\n\t\t\treturn nil, err\n\t\t}\n\t\tudpPacketCodec = serverHello.Selected.Message.UDPPacketCodec\n\t}\n\n\tvar loginRespMsg msg.LoginResp\n\tif err := rw.ReadMsgInto(&loginRespMsg); err != nil {\n\t\treturn nil, err\n\t}\n\treturn &loginExchangeResult{\n\t\tresp:           &loginRespMsg,\n\t\tcrypto:         cryptoContext,\n\t\tudpPacketCodec: udpPacketCodec,\n\t}, nil\n}","sourceCodeStart":173,"sourceCodeEnd":209,"githubUrl":"https://github.com/fatedier/frp/blob/6c8a8d0a97d03b44e9528d30b30c70cb9d61b405/client/control_session.go#L173-L209","documentation":"With transport.wireProtocol = v2, frpc sends a ClientHello frame and frps answers with a ServerHello frame; if that frame carries a non-empty Error string, the client aborts the connection with that message (client/control_session.go:191). This is a v2 protocol handshake rejection — the server understood the magic/frame layer but refused the session before crypto setup.","triggerScenarios":"wireProtocol v2 enabled on the client while the server rejects the negotiation: unsupported/unsupported-version v2 handshake, bootstrap options the server refuses (transport/TLS/TCPMux combination), or a protocol-level policy rejection communicated in ServerHello.Error.","commonSituations":"Client and server on different frp versions where the v2 handshake semantics changed; enabling v2 wire protocol against an frps build that validates hello contents strictly; middleboxes altering the stream so the server bails during hello.","solutions":["Align frpc and frps to the same version before using wireProtocol v2","Read the embedded server message — it is frps's own refusal reason","If v2 is optional in your setup, remove transport.wireProtocol=v2 to fall back to v1 and confirm connectivity","Capture frps logs during the handshake for the matching rejection"],"exampleFix":"# before (frpc.toml)\ntransport:\n  wireProtocol = \"v2\"   # server too old -> ServerHello.Error\n\n# after\ntransport:\n  wireProtocol = \"v2\"  # keep only after upgrading frps to the matching version","handlingStrategy":"try-catch","validationCode":null,"typeGuard":null,"tryCatchPattern":"_, err := d.exchangeLogin(conn, loginMsg)\nif err != nil {\n    // ServerHello.Error is a definitive v2 handshake rejection\n    if strings.Contains(err.Error(), \"handshake\") || wireProtoV2 {\n        log.Error(\"v2 handshake rejected by frps; falling back to wireProtocol v1 requires restart with changed config\")\n    }\n    return err\n}","preventionTips":["Only enable wireProtocol v2 on fleets where frps version is pinned to match","Stage protocol upgrades: upgrade frps first, then flip clients","Capture frps logs alongside client errors during v2 rollout"],"tags":["go","frpc","wire-protocol","v2","handshake"],"backgroundTag":null,"analyzedSha":"6c8a8d0a97d03b44e9528d30b30c70cb9d61b405","analyzedAt":"2026-08-15T06:53:27.215Z","schemaVersion":2},"datasetVersion":"2026-08-15T22:17:37.221Z"}