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
- Pass exactly the JSON payload bytes that were sent on the wire — Frame.Payload after ReadFrame, not the framed bytes.
- Validate the payload with json.Valid() before calling NewClientCryptoContext to get a clearer failure point.
- 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
- Store the exact wire bytes (Frame.Payload) for transcript use — never re-marshal decoded structs.
- The transcript hash must cover original bytes; any re-encoding breaks it even if JSON parses.
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
- decode ServerHello transcript: %w
- no supported crypto algorithm
- invalid crypto client random length %d, want %d
- unknown selected crypto algorithm: %s
- selected crypto algorithm was not advertised by client: %s
AI-assisted analysis of fatedier/frp@6c8a8d0a97 (2026-08-15).
Data as JSON: /api/errors/7c21497e8a4b1a74.
Report an issue: GitHub.