forked from quic-go/quic-go
* qlog: implement a Trace and a Writer struct * qlog: rename Trace to FileSeq * split qlog trace writer and QUIC qlog events into separate packages * use the new qlog.Recorder instead of the logging.ConnectionTracer
482 lines
16 KiB
Go
482 lines
16 KiB
Go
package qlog
|
|
|
|
import (
|
|
"encoding/hex"
|
|
|
|
"github.com/quic-go/quic-go/internal/wire"
|
|
"github.com/quic-go/quic-go/qlogwriter/jsontext"
|
|
)
|
|
|
|
type Frame struct {
|
|
Frame any
|
|
}
|
|
|
|
type frames []Frame
|
|
|
|
type (
|
|
// An AckFrame is an ACK frame.
|
|
AckFrame = wire.AckFrame
|
|
// A ConnectionCloseFrame is a CONNECTION_CLOSE frame.
|
|
ConnectionCloseFrame = wire.ConnectionCloseFrame
|
|
// A DataBlockedFrame is a DATA_BLOCKED frame.
|
|
DataBlockedFrame = wire.DataBlockedFrame
|
|
// A HandshakeDoneFrame is a HANDSHAKE_DONE frame.
|
|
HandshakeDoneFrame = wire.HandshakeDoneFrame
|
|
// A MaxDataFrame is a MAX_DATA frame.
|
|
MaxDataFrame = wire.MaxDataFrame
|
|
// A MaxStreamDataFrame is a MAX_STREAM_DATA frame.
|
|
MaxStreamDataFrame = wire.MaxStreamDataFrame
|
|
// A MaxStreamsFrame is a MAX_STREAMS_FRAME.
|
|
MaxStreamsFrame = wire.MaxStreamsFrame
|
|
// A NewConnectionIDFrame is a NEW_CONNECTION_ID frame.
|
|
NewConnectionIDFrame = wire.NewConnectionIDFrame
|
|
// A NewTokenFrame is a NEW_TOKEN frame.
|
|
NewTokenFrame = wire.NewTokenFrame
|
|
// A PathChallengeFrame is a PATH_CHALLENGE frame.
|
|
PathChallengeFrame = wire.PathChallengeFrame
|
|
// A PathResponseFrame is a PATH_RESPONSE frame.
|
|
PathResponseFrame = wire.PathResponseFrame
|
|
// A PingFrame is a PING frame.
|
|
PingFrame = wire.PingFrame
|
|
// A ResetStreamFrame is a RESET_STREAM frame.
|
|
ResetStreamFrame = wire.ResetStreamFrame
|
|
// A RetireConnectionIDFrame is a RETIRE_CONNECTION_ID frame.
|
|
RetireConnectionIDFrame = wire.RetireConnectionIDFrame
|
|
// A StopSendingFrame is a STOP_SENDING frame.
|
|
StopSendingFrame = wire.StopSendingFrame
|
|
// A StreamsBlockedFrame is a STREAMS_BLOCKED frame.
|
|
StreamsBlockedFrame = wire.StreamsBlockedFrame
|
|
// A StreamDataBlockedFrame is a STREAM_DATA_BLOCKED frame.
|
|
StreamDataBlockedFrame = wire.StreamDataBlockedFrame
|
|
// An AckFrequencyFrame is an ACK_FREQUENCY frame.
|
|
AckFrequencyFrame = wire.AckFrequencyFrame
|
|
// An ImmediateAckFrame is an IMMEDIATE_ACK frame.
|
|
ImmediateAckFrame = wire.ImmediateAckFrame
|
|
)
|
|
|
|
type AckRange = wire.AckRange
|
|
|
|
// A CryptoFrame is a CRYPTO frame.
|
|
type CryptoFrame struct {
|
|
Offset int64
|
|
Length int64
|
|
}
|
|
|
|
// A StreamFrame is a STREAM frame.
|
|
type StreamFrame struct {
|
|
StreamID StreamID
|
|
Offset int64
|
|
Length int64
|
|
Fin bool
|
|
}
|
|
|
|
// A DatagramFrame is a DATAGRAM frame.
|
|
type DatagramFrame struct {
|
|
Length int64
|
|
}
|
|
|
|
func (fs frames) Encode(enc *jsontext.Encoder) error {
|
|
h := encoderHelper{enc: enc}
|
|
h.WriteToken(jsontext.BeginArray)
|
|
for _, f := range fs {
|
|
if err := f.Encode(enc); err != nil {
|
|
return err
|
|
}
|
|
}
|
|
h.WriteToken(jsontext.EndArray)
|
|
return h.err
|
|
}
|
|
|
|
func (f Frame) Encode(enc *jsontext.Encoder) error {
|
|
switch frame := f.Frame.(type) {
|
|
case *PingFrame:
|
|
return encodePingFrame(enc, frame)
|
|
case *AckFrame:
|
|
return encodeAckFrame(enc, frame)
|
|
case *ResetStreamFrame:
|
|
return encodeResetStreamFrame(enc, frame)
|
|
case *StopSendingFrame:
|
|
return encodeStopSendingFrame(enc, frame)
|
|
case *CryptoFrame:
|
|
return encodeCryptoFrame(enc, frame)
|
|
case *NewTokenFrame:
|
|
return encodeNewTokenFrame(enc, frame)
|
|
case *StreamFrame:
|
|
return encodeStreamFrame(enc, frame)
|
|
case *MaxDataFrame:
|
|
return encodeMaxDataFrame(enc, frame)
|
|
case *MaxStreamDataFrame:
|
|
return encodeMaxStreamDataFrame(enc, frame)
|
|
case *MaxStreamsFrame:
|
|
return encodeMaxStreamsFrame(enc, frame)
|
|
case *DataBlockedFrame:
|
|
return encodeDataBlockedFrame(enc, frame)
|
|
case *StreamDataBlockedFrame:
|
|
return encodeStreamDataBlockedFrame(enc, frame)
|
|
case *StreamsBlockedFrame:
|
|
return encodeStreamsBlockedFrame(enc, frame)
|
|
case *NewConnectionIDFrame:
|
|
return encodeNewConnectionIDFrame(enc, frame)
|
|
case *RetireConnectionIDFrame:
|
|
return encodeRetireConnectionIDFrame(enc, frame)
|
|
case *PathChallengeFrame:
|
|
return encodePathChallengeFrame(enc, frame)
|
|
case *PathResponseFrame:
|
|
return encodePathResponseFrame(enc, frame)
|
|
case *ConnectionCloseFrame:
|
|
return encodeConnectionCloseFrame(enc, frame)
|
|
case *HandshakeDoneFrame:
|
|
return encodeHandshakeDoneFrame(enc, frame)
|
|
case *DatagramFrame:
|
|
return encodeDatagramFrame(enc, frame)
|
|
case *AckFrequencyFrame:
|
|
return encodeAckFrequencyFrame(enc, frame)
|
|
case *ImmediateAckFrame:
|
|
return encodeImmediateAckFrame(enc, frame)
|
|
default:
|
|
panic("unknown frame type")
|
|
}
|
|
}
|
|
|
|
func encodePingFrame(enc *jsontext.Encoder, _ *PingFrame) error {
|
|
h := encoderHelper{enc: enc}
|
|
h.WriteToken(jsontext.BeginObject)
|
|
h.WriteToken(jsontext.String("frame_type"))
|
|
h.WriteToken(jsontext.String("ping"))
|
|
h.WriteToken(jsontext.EndObject)
|
|
return h.err
|
|
}
|
|
|
|
type ackRanges []wire.AckRange
|
|
|
|
func (ars ackRanges) Encode(enc *jsontext.Encoder) error {
|
|
h := encoderHelper{enc: enc}
|
|
h.WriteToken(jsontext.BeginArray)
|
|
for _, r := range ars {
|
|
if err := ackRange(r).Encode(enc); err != nil {
|
|
return err
|
|
}
|
|
}
|
|
h.WriteToken(jsontext.EndArray)
|
|
return h.err
|
|
}
|
|
|
|
type ackRange wire.AckRange
|
|
|
|
func (ar ackRange) Encode(enc *jsontext.Encoder) error {
|
|
h := encoderHelper{enc: enc}
|
|
h.WriteToken(jsontext.BeginArray)
|
|
h.WriteToken(jsontext.Int(int64(ar.Smallest)))
|
|
if ar.Smallest != ar.Largest {
|
|
h.WriteToken(jsontext.Int(int64(ar.Largest)))
|
|
}
|
|
h.WriteToken(jsontext.EndArray)
|
|
return h.err
|
|
}
|
|
|
|
func encodeAckFrame(enc *jsontext.Encoder, f *AckFrame) error {
|
|
h := encoderHelper{enc: enc}
|
|
h.WriteToken(jsontext.BeginObject)
|
|
h.WriteToken(jsontext.String("frame_type"))
|
|
h.WriteToken(jsontext.String("ack"))
|
|
if f.DelayTime > 0 {
|
|
h.WriteToken(jsontext.String("ack_delay"))
|
|
h.WriteToken(jsontext.Float(milliseconds(f.DelayTime)))
|
|
}
|
|
h.WriteToken(jsontext.String("acked_ranges"))
|
|
if err := ackRanges(f.AckRanges).Encode(enc); err != nil {
|
|
return err
|
|
}
|
|
hasECN := f.ECT0 > 0 || f.ECT1 > 0 || f.ECNCE > 0
|
|
if hasECN {
|
|
h.WriteToken(jsontext.String("ect0"))
|
|
h.WriteToken(jsontext.Uint(f.ECT0))
|
|
h.WriteToken(jsontext.String("ect1"))
|
|
h.WriteToken(jsontext.Uint(f.ECT1))
|
|
h.WriteToken(jsontext.String("ce"))
|
|
h.WriteToken(jsontext.Uint(f.ECNCE))
|
|
}
|
|
h.WriteToken(jsontext.EndObject)
|
|
return h.err
|
|
}
|
|
|
|
func encodeResetStreamFrame(enc *jsontext.Encoder, f *ResetStreamFrame) error {
|
|
h := encoderHelper{enc: enc}
|
|
h.WriteToken(jsontext.BeginObject)
|
|
h.WriteToken(jsontext.String("frame_type"))
|
|
if f.ReliableSize > 0 {
|
|
h.WriteToken(jsontext.String("reset_stream_at"))
|
|
} else {
|
|
h.WriteToken(jsontext.String("reset_stream"))
|
|
}
|
|
h.WriteToken(jsontext.String("stream_id"))
|
|
h.WriteToken(jsontext.Int(int64(f.StreamID)))
|
|
h.WriteToken(jsontext.String("error_code"))
|
|
h.WriteToken(jsontext.Int(int64(f.ErrorCode)))
|
|
h.WriteToken(jsontext.String("final_size"))
|
|
h.WriteToken(jsontext.Int(int64(f.FinalSize)))
|
|
if f.ReliableSize > 0 {
|
|
h.WriteToken(jsontext.String("reliable_size"))
|
|
h.WriteToken(jsontext.Int(int64(f.ReliableSize)))
|
|
}
|
|
h.WriteToken(jsontext.EndObject)
|
|
return h.err
|
|
}
|
|
|
|
func encodeStopSendingFrame(enc *jsontext.Encoder, f *StopSendingFrame) error {
|
|
h := encoderHelper{enc: enc}
|
|
h.WriteToken(jsontext.BeginObject)
|
|
h.WriteToken(jsontext.String("frame_type"))
|
|
h.WriteToken(jsontext.String("stop_sending"))
|
|
h.WriteToken(jsontext.String("stream_id"))
|
|
h.WriteToken(jsontext.Int(int64(f.StreamID)))
|
|
h.WriteToken(jsontext.String("error_code"))
|
|
h.WriteToken(jsontext.Int(int64(f.ErrorCode)))
|
|
h.WriteToken(jsontext.EndObject)
|
|
return h.err
|
|
}
|
|
|
|
func encodeCryptoFrame(enc *jsontext.Encoder, f *CryptoFrame) error {
|
|
h := encoderHelper{enc: enc}
|
|
h.WriteToken(jsontext.BeginObject)
|
|
h.WriteToken(jsontext.String("frame_type"))
|
|
h.WriteToken(jsontext.String("crypto"))
|
|
h.WriteToken(jsontext.String("offset"))
|
|
h.WriteToken(jsontext.Int(f.Offset))
|
|
h.WriteToken(jsontext.String("length"))
|
|
h.WriteToken(jsontext.Int(f.Length))
|
|
h.WriteToken(jsontext.EndObject)
|
|
return h.err
|
|
}
|
|
|
|
func encodeNewTokenFrame(enc *jsontext.Encoder, f *NewTokenFrame) error {
|
|
h := encoderHelper{enc: enc}
|
|
h.WriteToken(jsontext.BeginObject)
|
|
h.WriteToken(jsontext.String("frame_type"))
|
|
h.WriteToken(jsontext.String("new_token"))
|
|
h.WriteToken(jsontext.String("token"))
|
|
if err := (Token{Raw: f.Token}).Encode(enc); err != nil {
|
|
return err
|
|
}
|
|
h.WriteToken(jsontext.EndObject)
|
|
return h.err
|
|
}
|
|
|
|
func encodeStreamFrame(enc *jsontext.Encoder, f *StreamFrame) error {
|
|
h := encoderHelper{enc: enc}
|
|
h.WriteToken(jsontext.BeginObject)
|
|
h.WriteToken(jsontext.String("frame_type"))
|
|
h.WriteToken(jsontext.String("stream"))
|
|
h.WriteToken(jsontext.String("stream_id"))
|
|
h.WriteToken(jsontext.Int(int64(f.StreamID)))
|
|
h.WriteToken(jsontext.String("offset"))
|
|
h.WriteToken(jsontext.Int(f.Offset))
|
|
h.WriteToken(jsontext.String("length"))
|
|
h.WriteToken(jsontext.Int(f.Length))
|
|
if f.Fin {
|
|
h.WriteToken(jsontext.String("fin"))
|
|
h.WriteToken(jsontext.True)
|
|
}
|
|
h.WriteToken(jsontext.EndObject)
|
|
return h.err
|
|
}
|
|
|
|
func encodeMaxDataFrame(enc *jsontext.Encoder, f *MaxDataFrame) error {
|
|
h := encoderHelper{enc: enc}
|
|
h.WriteToken(jsontext.BeginObject)
|
|
h.WriteToken(jsontext.String("frame_type"))
|
|
h.WriteToken(jsontext.String("max_data"))
|
|
h.WriteToken(jsontext.String("maximum"))
|
|
h.WriteToken(jsontext.Int(int64(f.MaximumData)))
|
|
h.WriteToken(jsontext.EndObject)
|
|
return h.err
|
|
}
|
|
|
|
func encodeMaxStreamDataFrame(enc *jsontext.Encoder, f *MaxStreamDataFrame) error {
|
|
h := encoderHelper{enc: enc}
|
|
h.WriteToken(jsontext.BeginObject)
|
|
h.WriteToken(jsontext.String("frame_type"))
|
|
h.WriteToken(jsontext.String("max_stream_data"))
|
|
h.WriteToken(jsontext.String("stream_id"))
|
|
h.WriteToken(jsontext.Int(int64(f.StreamID)))
|
|
h.WriteToken(jsontext.String("maximum"))
|
|
h.WriteToken(jsontext.Int(int64(f.MaximumStreamData)))
|
|
h.WriteToken(jsontext.EndObject)
|
|
return h.err
|
|
}
|
|
|
|
func encodeMaxStreamsFrame(enc *jsontext.Encoder, f *MaxStreamsFrame) error {
|
|
h := encoderHelper{enc: enc}
|
|
h.WriteToken(jsontext.BeginObject)
|
|
h.WriteToken(jsontext.String("frame_type"))
|
|
h.WriteToken(jsontext.String("max_streams"))
|
|
h.WriteToken(jsontext.String("stream_type"))
|
|
h.WriteToken(jsontext.String(streamType(f.Type).String()))
|
|
h.WriteToken(jsontext.String("maximum"))
|
|
h.WriteToken(jsontext.Int(int64(f.MaxStreamNum)))
|
|
h.WriteToken(jsontext.EndObject)
|
|
return h.err
|
|
}
|
|
|
|
func encodeDataBlockedFrame(enc *jsontext.Encoder, f *DataBlockedFrame) error {
|
|
h := encoderHelper{enc: enc}
|
|
h.WriteToken(jsontext.BeginObject)
|
|
h.WriteToken(jsontext.String("frame_type"))
|
|
h.WriteToken(jsontext.String("data_blocked"))
|
|
h.WriteToken(jsontext.String("limit"))
|
|
h.WriteToken(jsontext.Int(int64(f.MaximumData)))
|
|
h.WriteToken(jsontext.EndObject)
|
|
return h.err
|
|
}
|
|
|
|
func encodeStreamDataBlockedFrame(enc *jsontext.Encoder, f *StreamDataBlockedFrame) error {
|
|
h := encoderHelper{enc: enc}
|
|
h.WriteToken(jsontext.BeginObject)
|
|
h.WriteToken(jsontext.String("frame_type"))
|
|
h.WriteToken(jsontext.String("stream_data_blocked"))
|
|
h.WriteToken(jsontext.String("stream_id"))
|
|
h.WriteToken(jsontext.Int(int64(f.StreamID)))
|
|
h.WriteToken(jsontext.String("limit"))
|
|
h.WriteToken(jsontext.Int(int64(f.MaximumStreamData)))
|
|
h.WriteToken(jsontext.EndObject)
|
|
return h.err
|
|
}
|
|
|
|
func encodeStreamsBlockedFrame(enc *jsontext.Encoder, f *StreamsBlockedFrame) error {
|
|
h := encoderHelper{enc: enc}
|
|
h.WriteToken(jsontext.BeginObject)
|
|
h.WriteToken(jsontext.String("frame_type"))
|
|
h.WriteToken(jsontext.String("streams_blocked"))
|
|
h.WriteToken(jsontext.String("stream_type"))
|
|
h.WriteToken(jsontext.String(streamType(f.Type).String()))
|
|
h.WriteToken(jsontext.String("limit"))
|
|
h.WriteToken(jsontext.Int(int64(f.StreamLimit)))
|
|
h.WriteToken(jsontext.EndObject)
|
|
return h.err
|
|
}
|
|
|
|
func encodeNewConnectionIDFrame(enc *jsontext.Encoder, f *NewConnectionIDFrame) error {
|
|
h := encoderHelper{enc: enc}
|
|
h.WriteToken(jsontext.BeginObject)
|
|
h.WriteToken(jsontext.String("frame_type"))
|
|
h.WriteToken(jsontext.String("new_connection_id"))
|
|
h.WriteToken(jsontext.String("sequence_number"))
|
|
h.WriteToken(jsontext.Uint(f.SequenceNumber))
|
|
h.WriteToken(jsontext.String("retire_prior_to"))
|
|
h.WriteToken(jsontext.Uint(f.RetirePriorTo))
|
|
h.WriteToken(jsontext.String("length"))
|
|
h.WriteToken(jsontext.Int(int64(f.ConnectionID.Len())))
|
|
h.WriteToken(jsontext.String("connection_id"))
|
|
h.WriteToken(jsontext.String(f.ConnectionID.String()))
|
|
h.WriteToken(jsontext.String("stateless_reset_token"))
|
|
h.WriteToken(jsontext.String(hex.EncodeToString(f.StatelessResetToken[:])))
|
|
h.WriteToken(jsontext.EndObject)
|
|
return h.err
|
|
}
|
|
|
|
func encodeRetireConnectionIDFrame(enc *jsontext.Encoder, f *RetireConnectionIDFrame) error {
|
|
h := encoderHelper{enc: enc}
|
|
h.WriteToken(jsontext.BeginObject)
|
|
h.WriteToken(jsontext.String("frame_type"))
|
|
h.WriteToken(jsontext.String("retire_connection_id"))
|
|
h.WriteToken(jsontext.String("sequence_number"))
|
|
h.WriteToken(jsontext.Uint(f.SequenceNumber))
|
|
h.WriteToken(jsontext.EndObject)
|
|
return h.err
|
|
}
|
|
|
|
func encodePathChallengeFrame(enc *jsontext.Encoder, f *PathChallengeFrame) error {
|
|
h := encoderHelper{enc: enc}
|
|
h.WriteToken(jsontext.BeginObject)
|
|
h.WriteToken(jsontext.String("frame_type"))
|
|
h.WriteToken(jsontext.String("path_challenge"))
|
|
h.WriteToken(jsontext.String("data"))
|
|
h.WriteToken(jsontext.String(hex.EncodeToString(f.Data[:])))
|
|
h.WriteToken(jsontext.EndObject)
|
|
return h.err
|
|
}
|
|
|
|
func encodePathResponseFrame(enc *jsontext.Encoder, f *PathResponseFrame) error {
|
|
h := encoderHelper{enc: enc}
|
|
h.WriteToken(jsontext.BeginObject)
|
|
h.WriteToken(jsontext.String("frame_type"))
|
|
h.WriteToken(jsontext.String("path_response"))
|
|
h.WriteToken(jsontext.String("data"))
|
|
h.WriteToken(jsontext.String(hex.EncodeToString(f.Data[:])))
|
|
h.WriteToken(jsontext.EndObject)
|
|
return h.err
|
|
}
|
|
|
|
func encodeConnectionCloseFrame(enc *jsontext.Encoder, f *ConnectionCloseFrame) error {
|
|
h := encoderHelper{enc: enc}
|
|
h.WriteToken(jsontext.BeginObject)
|
|
h.WriteToken(jsontext.String("frame_type"))
|
|
h.WriteToken(jsontext.String("connection_close"))
|
|
h.WriteToken(jsontext.String("error_space"))
|
|
errorSpace := "transport"
|
|
if f.IsApplicationError {
|
|
errorSpace = "application"
|
|
}
|
|
h.WriteToken(jsontext.String(errorSpace))
|
|
errName := transportError(f.ErrorCode).String()
|
|
if len(errName) > 0 {
|
|
h.WriteToken(jsontext.String("error_code"))
|
|
h.WriteToken(jsontext.String(errName))
|
|
} else {
|
|
h.WriteToken(jsontext.String("error_code"))
|
|
h.WriteToken(jsontext.Uint(f.ErrorCode))
|
|
}
|
|
h.WriteToken(jsontext.String("raw_error_code"))
|
|
h.WriteToken(jsontext.Uint(f.ErrorCode))
|
|
h.WriteToken(jsontext.String("reason"))
|
|
h.WriteToken(jsontext.String(f.ReasonPhrase))
|
|
h.WriteToken(jsontext.EndObject)
|
|
return h.err
|
|
}
|
|
|
|
func encodeHandshakeDoneFrame(enc *jsontext.Encoder, _ *HandshakeDoneFrame) error {
|
|
h := encoderHelper{enc: enc}
|
|
h.WriteToken(jsontext.BeginObject)
|
|
h.WriteToken(jsontext.String("frame_type"))
|
|
h.WriteToken(jsontext.String("handshake_done"))
|
|
h.WriteToken(jsontext.EndObject)
|
|
return h.err
|
|
}
|
|
|
|
func encodeDatagramFrame(enc *jsontext.Encoder, f *DatagramFrame) error {
|
|
h := encoderHelper{enc: enc}
|
|
h.WriteToken(jsontext.BeginObject)
|
|
h.WriteToken(jsontext.String("frame_type"))
|
|
h.WriteToken(jsontext.String("datagram"))
|
|
h.WriteToken(jsontext.String("length"))
|
|
h.WriteToken(jsontext.Int(f.Length))
|
|
h.WriteToken(jsontext.EndObject)
|
|
return h.err
|
|
}
|
|
|
|
func encodeAckFrequencyFrame(enc *jsontext.Encoder, f *AckFrequencyFrame) error {
|
|
h := encoderHelper{enc: enc}
|
|
h.WriteToken(jsontext.BeginObject)
|
|
h.WriteToken(jsontext.String("frame_type"))
|
|
h.WriteToken(jsontext.String("ack_frequency"))
|
|
h.WriteToken(jsontext.String("sequence_number"))
|
|
h.WriteToken(jsontext.Uint(f.SequenceNumber))
|
|
h.WriteToken(jsontext.String("ack_eliciting_threshold"))
|
|
h.WriteToken(jsontext.Uint(f.AckElicitingThreshold))
|
|
h.WriteToken(jsontext.String("request_max_ack_delay"))
|
|
h.WriteToken(jsontext.Float(milliseconds(f.RequestMaxAckDelay)))
|
|
h.WriteToken(jsontext.String("reordering_threshold"))
|
|
h.WriteToken(jsontext.Int(int64(f.ReorderingThreshold)))
|
|
h.WriteToken(jsontext.EndObject)
|
|
return h.err
|
|
}
|
|
|
|
func encodeImmediateAckFrame(enc *jsontext.Encoder, _ *ImmediateAckFrame) error {
|
|
h := encoderHelper{enc: enc}
|
|
h.WriteToken(jsontext.BeginObject)
|
|
h.WriteToken(jsontext.String("frame_type"))
|
|
h.WriteToken(jsontext.String("immediate_ack"))
|
|
h.WriteToken(jsontext.EndObject)
|
|
return h.err
|
|
}
|