fatedier/frp · error

decode ServerHello transcript: %w

Error message

decode ServerHello transcript: %w

What it means

Thrown by NewClientCryptoContext when the stored ServerHello transcript payload fails to json.Unmarshal. Like the client-hello check, it guards that the transcript bytes used for the transcript hash are the exact JSON the server sent.

Source

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

	}
	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)
}

func writeCryptoTranscriptPart(h hash.Hash, label string, payload []byte) {
	var length [8]byte

View on GitHub (pinned to 6c8a8d0a97)

Solutions

  1. Read the ServerHello with ReadJSONFrame(frameTypeServerHello, &hello) and keep the exact f.Payload bytes for NewCryptoContext.
  2. Log the offending payload (truncated) when this error fires — the wrapped json error tells you the byte offset of the syntax problem.
  3. Ensure the server actually completed its hello before the client captures the bytes.

Example fix

// before
var hello wire.ServerHello
_ = conn.ReadJSONFrame(wire.FrameTypeServerHello, &hello)
raw, _ := json.Marshal(hello) // re-marshaled, not the wire bytes
ctx, err := wire.NewClientCryptoContext(clientRaw, raw)

// after
f, err := conn.ReadFrame()
if err != nil { return err }
ctx, err := wire.NewClientCryptoContext(clientRaw, f.Payload)
Defensive patterns

Strategy: validation

Validate before calling

if !json.Valid(serverHelloPayload) {
    return errors.New("server hello transcript is not valid JSON")
}

Try / catch

if _, err := wire.NewClientCryptoContext(clientRaw, serverRaw); err != nil {
    if strings.HasPrefix(err.Error(), "decode ServerHello") {
        // log truncated payload; likely wrong frame captured or server died mid-hello
    }
}

Prevention

When it happens

Trigger: Passing a serverHelloPayload that is not valid ServerHello JSON: truncated frame payload, wrong frame's payload, a plain-text error page read instead of the hello, or an empty slice.

Common situations: Reading the server hello with the wrong frame type expectation and feeding whatever came first; a server that closed mid-handshake leaving a partial payload; tests hand-crafting the server payload as a map that marshals differently than expected.

Related errors


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