fatedier/frp · critical
unexpected frame type %d, want %d
Error message
unexpected frame type %d, want %d
What it means
During the wire-protocol login handshake, the first frame frpc reads from the connection must be a FrameTypeServerHello. Any other frame type (or bytes that decode to a different type) fails here. It means the peer on the other end of the connection is not speaking the expected frps wire handshake as the very first message.
Source
Thrown at client/control_session.go:184
}
if err := rw.WriteMsg(loginMsg); err != nil {
return nil, err
}
_ = conn.SetReadDeadline(time.Now().Add(10 * time.Second))
defer func() {
_ = 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, errView on GitHub (pinned to 6c8a8d0a97)
Solutions
- Verify serverAddr/serverPort point to the frps bind port (bindPort), not the webServer/dashboard port
- Upgrade frpc and frps to the same version so both sides speak the same handshake
- Remove intermediary proxies/LBs that inject their own protocol bytes on that connection
- Test connectivity directly: nc <server> <port> should not immediately return HTTP text
Example fix
# frpc.toml — before serverAddr = "example.com" serverPort = 7500 # dashboard port, wrong # after serverAddr = "example.com" serverPort = 7000 # frps bindPort
Defensive patterns
Strategy: validation
Validate before calling
func portSpeaksFrp(addr string, timeout time.Duration) bool {
conn, err := net.DialTimeout("tcp", addr, timeout)
if err != nil {
return false
}
defer conn.Close()
_ = conn.SetReadDeadline(time.Now().Add(2 * time.Second))
buf := make([]byte, 16)
n, _ := conn.Read(buf)
// an HTTP greeting (e.g. 'HTTP/', 'GET') means wrong port
s := string(buf[:n])
return !strings.HasPrefix(s, "HTTP/") && !strings.Contains(s, "GET ")
} Prevention
- Point serverPort at frps bindPort, never the dashboard/webServer port
- Keep frpc/frps versions matched across upgrades
- Prefer direct connectivity over protocol-mangling proxies for the control connection
When it happens
Trigger: Connecting to a port served by something other than a compatible frps: an older frps without the hello-first handshake, an HTTP server, or a general TCP service; or a proxy in front returning its own protocol bytes first. Only on the path where wireConn is non-nil (modern wire protocol).
Common situations: Wrong serverAddr/serverPort pointing at an frps bind port vs dashboard port, or an entirely unrelated service; frpc much newer than frps (or vice versa) with handshake-order changes; connecting through a forward proxy that sends a greeting first.
Related errors
- create control crypto read writer: %w
- loginRespMsg.Error
- serverHello.Error
- send ${op} request to plugin error
- invalid argument: frpc has no config file path
AI-assisted analysis of fatedier/frp@6c8a8d0a97 (2026-08-15).
Data as JSON: /api/errors/8e803b92f2198381.
Report an issue: GitHub.