fatedier/frp · error

unexpected message type

Error message

unexpected message type %s, want %s

What it means

Thrown by DecodeV2MessageFrameInto in pkg/msg/wire_v2.go when the frame's message type ID is a known type but does not equal the expected ID for the caller's target struct. The decoder is strict: if you decode into *msg.Ping but the frame carries msg.NatHoleVisitor, you get 'unexpected message type NatHoleVisitor, want Ping' instead of silently unmarshaling one message into another struct's fields.

Solutions

  1. Use the generic decoder (DecodeV2MessageFrame) or the transport layer's Do() with an expected-response type when order is not guaranteed, then type-switch on the result
  2. Read and dispatch messages by their type ID instead of assuming the next message is a specific type
  3. If an error reply is possible, expect it: type-switch and check for the error/resp variant before asserting the happy-path type
  4. Check the error message text: it names both the actual and expected types, telling you exactly what arrived

Example fix

// before
var pong msg.Pong
_ = msg.DecodeV2MessageFrameInto(frame, &pong) // fails if server sent LoginResp

// after
m, err := msg.DecodeV2MessageFrame(frame)
if err != nil {
    return err
}
switch v := m.(type) {
case *msg.Pong:
    handlePong(v)
case *msg.LoginResp:
    handleLoginResp(v)
}
Defensive patterns

Strategy: type-guard

Type guard

func asPong(m msg.Message) (*msg.Pong, bool) {
    p, ok := m.(*msg.Pong)
    return p, ok
}

Try / catch

m, err := msg.DecodeV2MessageFrame(frame)
if err != nil {
    return err
}
if v, ok := m.(*msg.Ping); ok {
    return handlePing(v)
}
return fmt.Errorf("unexpected message %T", m)

Prevention

When it happens

Trigger: Calling msg.DecodeV2MessageFrameInto(f, out) where out's element type maps to a different type ID than the one in the frame. Typical when a read loop assumes a fixed response type but the peer sends a different message first (e.g. an error response, a pong instead of a login resp), or when message ordering assumptions break.

Common situations: Request/response code that expects exactly one message kind on a connection that multiplexes several; races where a control message arrives between a request and its expected reply; test code reusing one target struct for multiple message types.

Related errors


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

Appendix: source

Thrown at pkg/msg/wire_v2.go:165

		return fmt.Errorf("message frame payload too short")
	}

	typeID := binary.BigEndian.Uint16(f.Payload[:2])
	outType := reflect.TypeOf(out)
	if outType == nil || outType.Kind() != reflect.Pointer {
		return fmt.Errorf("message target must be a pointer")
	}
	elemType := outType.Elem()
	expectedTypeID, ok := v2MsgTypeIDMap[elemType]
	if !ok {
		return fmt.Errorf("unknown v2 message type %s", elemType.String())
	}
	if typeID != expectedTypeID {
		actualType, ok := v2MsgReflectTypeMap[typeID]
		if !ok {
			return fmt.Errorf("unknown v2 message type %d", typeID)
		}
		return fmt.Errorf("unexpected message type %s, want %s", actualType.String(), elemType.String())
	}
	return json.Unmarshal(f.Payload[2:], out)
}

func EncodeV2MessageFrame(m Message) (*wire.Frame, error) {
	t := reflect.TypeOf(m)
	if t == nil {
		return nil, fmt.Errorf("nil message")
	}
	if t.Kind() == reflect.Pointer {
		t = t.Elem()
	}
	typeID, ok := v2MsgTypeIDMap[t]
	if !ok {
		return nil, fmt.Errorf("unknown v2 message type %s", t.String())
	}
	content, err := json.Marshal(m)
	if err != nil {

View on GitHub (pinned to 6c8a8d0a97)