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
- 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
- Read and dispatch messages by their type ID instead of assuming the next message is a specific type
- If an error reply is possible, expect it: type-switch and check for the error/resp variant before asserting the happy-path type
- 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
- Prefer generic decode + type-switch over decode-into when message order is not guaranteed
- Use transport Do() with expected response types for RPC-style exchanges
- Handle error/response variants before asserting the happy-path type
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
- unmarshal ProxyConfig error: %v
- unmarshal VisitorConfig error: %v
- unmarshal ClientPluginOptions error: %v
- unmarshal VisitorPluginOptions error: %v
- invalid transport.protocol, optional values are %v
AI-assisted analysis of fatedier/frp@6c8a8d0a97 (2026-08-15).
Data as JSON: /api/errors/0ba72e6033cc95dd.
Report an issue: GitHub.