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, err

View on GitHub (pinned to 6c8a8d0a97)

Solutions

  1. Verify serverAddr/serverPort point to the frps bind port (bindPort), not the webServer/dashboard port
  2. Upgrade frpc and frps to the same version so both sides speak the same handshake
  3. Remove intermediary proxies/LBs that inject their own protocol bytes on that connection
  4. 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

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


AI-assisted analysis of fatedier/frp@6c8a8d0a97 (2026-08-15). Data as JSON: /api/errors/8e803b92f2198381. Report an issue: GitHub.