fatedier/frp · error

unexpected message type %s, want %s

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.

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)

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.