fatedier/frp · error
serverHello.Error
Error message
serverHello.Error
What it means
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.
Source
Thrown at client/control_session.go:191
_ = conn.SetReadDeadline(time.Time{})
}()
var cryptoContext *wire.CryptoContext
var udpPacketCodec string
if wireConn != nil {
serverHelloFrame, err := wireConn.ReadFrame()
if err != nil {
return nil, err
}
if serverHelloFrame.Type != wire.FrameTypeServerHello {
return nil, fmt.Errorf("unexpected frame type %d, want %d", serverHelloFrame.Type, wire.FrameTypeServerHello)
}
var serverHello wire.ServerHello
if err := wireConn.UnmarshalFrame(serverHelloFrame, &serverHello); err != nil {
return nil, err
}
if serverHello.Error != "" {
return nil, errors.New(serverHello.Error)
}
cryptoContext, err = wire.NewClientCryptoContext(clientHelloPayload, serverHelloFrame.Payload)
if err != nil {
return nil, err
}
udpPacketCodec = serverHello.Selected.Message.UDPPacketCodec
}
var loginRespMsg msg.LoginResp
if err := rw.ReadMsgInto(&loginRespMsg); err != nil {
return nil, err
}
return &loginExchangeResult{
resp: &loginRespMsg,
crypto: cryptoContext,
udpPacketCodec: udpPacketCodec,
}, nil
}View on GitHub (pinned to 6c8a8d0a97)
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
Example fix
# before (frpc.toml) transport: wireProtocol = "v2" # server too old -> ServerHello.Error # after transport: wireProtocol = "v2" # keep only after upgrading frps to the matching version
Defensive patterns
Strategy: try-catch
Try / catch
_, err := d.exchangeLogin(conn, loginMsg)
if err != nil {
// ServerHello.Error is a definitive v2 handshake rejection
if strings.Contains(err.Error(), "handshake") || wireProtoV2 {
log.Error("v2 handshake rejected by frps; falling back to wireProtocol v1 requires restart with changed config")
}
return err
} Prevention
- 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
When it happens
Trigger: 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.
Common situations: 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.
Related errors
AI-assisted analysis of fatedier/frp@6c8a8d0a97 (2026-08-15).
Data as JSON: /api/errors/567abc861c3a6768.
Report an issue: GitHub.