fatedier/frp · error

decode ClientHello transcript: %w

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.

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)

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.