fatedier/frp · error

decode ClientHello transcript

Error message

decode ClientHello transcript: %w

What it means

Thrown by NewClientCryptoContext when the stored ClientHello transcript payload fails to json.Unmarshal. The function re-decodes both handshake payloads to build the client's CryptoContext, so the client hello bytes must still be valid JSON of the ClientHello shape.

Solutions

  1. Pass exactly the JSON payload bytes that were sent on the wire — Frame.Payload after ReadFrame, not the framed bytes.
  2. Validate the payload with json.Valid() before calling NewClientCryptoContext to get a clearer failure point.
  3. If replaying a captured session, capture payloads at the frame layer, before any transformation.

Example fix

// before
ctx, err := wire.NewClientCryptoContext(frameBytes, serverBytes) // frameBytes includes 8-byte header

// after
f, _ := conn.ReadFrame()
ctx, err := wire.NewClientCryptoContext(f.Payload, serverBytes)
Defensive patterns

Strategy: validation

Validate before calling

if !json.Valid(clientHelloPayload) {
    return errors.New("client hello transcript is not valid JSON — capture Frame.Payload only")
}

Try / catch

if _, err := wire.NewClientCryptoContext(clientRaw, serverRaw); err != nil {
    if strings.HasPrefix(err.Error(), "decode ClientHello") {
        // wrong bytes captured: re-capture payload at frame layer, do not retry blindly
    }
}

Prevention

When it happens

Trigger: Passing a clientHelloPayload that was modified, truncated, or captured from a different protocol version; passing the raw frame header plus JSON instead of payload only; passing nil or an empty slice.

Common situations: Test code that captures the wrong byte slice (e.g. the whole frame instead of Frame.Payload); a relay or proxy that re-encodes the payload; logging code that strips characters from the payload before replay.

Related errors


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

Appendix: source

Thrown at pkg/proto/wire/crypto.go:135

func selectUDPPacketCodec(codecs []string) string {
	if Supports(codecs, UDPPacketCodecBinary) {
		return UDPPacketCodecBinary
	}
	return ""
}

func NewCryptoContext(algorithm string, clientHelloPayload, serverHelloPayload []byte) *CryptoContext {
	return &CryptoContext{
		Algorithm:      algorithm,
		TranscriptHash: HashCryptoTranscript(clientHelloPayload, serverHelloPayload),
	}
}

func NewClientCryptoContext(clientHelloPayload, serverHelloPayload []byte) (*CryptoContext, error) {
	var clientHello ClientHello
	if err := json.Unmarshal(clientHelloPayload, &clientHello); err != nil {
		return nil, fmt.Errorf("decode ClientHello transcript: %w", err)
	}
	var serverHello ServerHello
	if err := json.Unmarshal(serverHelloPayload, &serverHello); err != nil {
		return nil, fmt.Errorf("decode ServerHello transcript: %w", err)
	}
	if err := ValidateServerHelloForClient(clientHello, serverHello); err != nil {
		return nil, err
	}

	return NewCryptoContext(serverHello.Selected.Crypto.Algorithm, clientHelloPayload, serverHelloPayload), nil
}

func HashCryptoTranscript(clientHelloPayload, serverHelloPayload []byte) []byte {
	h := sha256.New()
	_, _ = h.Write([]byte(cryptoTranscriptLabel))
	writeCryptoTranscriptPart(h, "client hello", clientHelloPayload)
	writeCryptoTranscriptPart(h, "server hello", serverHelloPayload)
	return h.Sum(nil)

View on GitHub (pinned to 6c8a8d0a97)