Repository: quic-go/quic-go
Pull request #5265 (2025-07-21 13:42:05)

wire: implement parsing and writing of the IMMEDIATE_ACK frame
For https://github.com/quic-go/quic-go/issues/2482.


Patch:
diff --git a/internal/wire/frame_parser.go b/internal/wire/frame_parser.go
index 9ccdc86c..afc3bedb 100644
--- a/internal/wire/frame_parser.go
+++ b/internal/wire/frame_parser.go
@@ -55,7 +55,7 @@ func (p *FrameParser) ParseType(b []byte, encLevel protocol.EncryptionLevel) (Fr
 		valid := ft.isValidRFC9000() ||
 			(p.supportsDatagrams && ft.IsDatagramFrameType()) ||
 			(p.supportsResetStreamAt && ft == FrameTypeResetStreamAt) ||
-			(p.supportsAckFrequency && ft == FrameTypeAckFrequency)
+			(p.supportsAckFrequency && (ft == FrameTypeAckFrequency || ft == FrameTypeImmediateAck))
 		if !valid {
 			return 0, parsed, &qerr.TransportError{
 				ErrorCode:    qerr.FrameEncodingError,
@@ -163,6 +163,8 @@ func (p *FrameParser) ParseLessCommonFrame(frameType FrameType, data []byte, v p
 		frame, l, err = parseResetStreamFrame(data, true, v)
 	case FrameTypeAckFrequency:
 		frame, l, err = parseAckFrequencyFrame(data, v)
+	case FrameTypeImmediateAck:
+		frame = &ImmediateAckFrame{}
 	default:
 		err = errUnknownFrameType
 	}
diff --git a/internal/wire/frame_type.go b/internal/wire/frame_type.go
index 25a269d0..d3d086d9 100644
--- a/internal/wire/frame_type.go
+++ b/internal/wire/frame_type.go
@@ -30,8 +30,11 @@ const (
 	FrameTypeConnectionClose    FrameType = 0x1c
 	FrameTypeApplicationClose   FrameType = 0x1d
 	FrameTypeHandshakeDone      FrameType = 0x1e
-	FrameTypeResetStreamAt      FrameType = 0x24 // https://datatracker.ietf.org/doc/draft-ietf-quic-reliable-stream-reset/06/
-	FrameTypeAckFrequency       FrameType = 0xaf // https://datatracker.ietf.org/doc/draft-ietf-quic-ack-frequency/11/
+	// https://datatracker.ietf.org/doc/draft-ietf-quic-reliable-stream-reset/07/
+	FrameTypeResetStreamAt FrameType = 0x24
+	// https://datatracker.ietf.org/doc/draft-ietf-quic-ack-frequency/11/
+	FrameTypeAckFrequency FrameType = 0xaf
+	FrameTypeImmediateAck FrameType = 0x1f
 
 	FrameTypeDatagramNoLength   FrameType = 0x30
 	FrameTypeDatagramWithLength FrameType = 0x31
diff --git a/internal/wire/immediate_ack_frame.go b/internal/wire/immediate_ack_frame.go
new file mode 100644
index 00000000..aeff71b4
--- /dev/null
+++ b/internal/wire/immediate_ack_frame.go
@@ -0,0 +1,18 @@
+package wire
+
+import (
+	"github.com/quic-go/quic-go/internal/protocol"
+	"github.com/quic-go/quic-go/quicvarint"
+)
+
+// An ImmediateAckFrame is an IMMEDIATE_ACK frame
+type ImmediateAckFrame struct{}
+
+func (f *ImmediateAckFrame) Append(b []byte, _ protocol.Version) ([]byte, error) {
+	return quicvarint.Append(b, uint64(FrameTypeImmediateAck)), nil
+}
+
+// Length of a written frame
+func (f *ImmediateAckFrame) Length(_ protocol.Version) protocol.ByteCount {
+	return protocol.ByteCount(quicvarint.Len(uint64(FrameTypeImmediateAck)))
+}


Test patch:
diff --git a/internal/ackhandler/ack_eliciting_test.go b/internal/ackhandler/ack_eliciting_test.go
index cf4c6315..7bb839c3 100644
--- a/internal/ackhandler/ack_eliciting_test.go
+++ b/internal/ackhandler/ack_eliciting_test.go
@@ -43,6 +43,7 @@ func TestIsFrameTypeAckEliciting(t *testing.T) {
 		wire.FrameTypeDatagramNoLength:   true,
 		wire.FrameTypeDatagramWithLength: true,
 		wire.FrameTypeAckFrequency:       true,
+		wire.FrameTypeImmediateAck:       true,
 	}
 
 	for ft, expected := range testCases {
@@ -63,6 +64,7 @@ func TestAckElicitingFrames(t *testing.T) {
 		&wire.MaxStreamDataFrame{}:   true,
 		&wire.StopSendingFrame{}:     true,
 		&wire.AckFrequencyFrame{}:    true,
+		&wire.ImmediateAckFrame{}:    true,
 	}
 
 	for f, expected := range testCases {
diff --git a/internal/wire/frame_parser_test.go b/internal/wire/frame_parser_test.go
index b060c0ca..b6f84e53 100644
--- a/internal/wire/frame_parser_test.go
+++ b/internal/wire/frame_parser_test.go
@@ -330,6 +330,11 @@ func TestFrameParserFrames(t *testing.T) {
 				ReorderingThreshold:   0xcafe,
 			},
 		},
+		{
+			name:      "IMMEDIATE_ACK",
+			frameType: FrameTypeImmediateAck,
+			frame:     &ImmediateAckFrame{},
+		},
 	}
 
 	for _, test := range tests {
@@ -523,7 +528,32 @@ func TestFrameParserResetStreamAtUnsupported(t *testing.T) {
 	require.NoError(t, err)
 
 	_, _, err = parser.ParseType(b, protocol.Encryption1RTT)
-	checkFrameUnsupported(t, err, 0x24)
+	checkFrameUnsupported(t, err, uint64(FrameTypeResetStreamAt))
+}
+
+func TestFrameParserAckFrequencyUnsupported(t *testing.T) {
+	parser := NewFrameParser(true, true, false)
+
+	t.Run("ACK_FREQUENCY", func(t *testing.T) {
+		f := &AckFrequencyFrame{
+			SequenceNumber:        1337,
+			AckElicitingThreshold: 42,
+			RequestMaxAckDelay:    42 * time.Millisecond,
+			ReorderingThreshold:   1234,
+		}
+		b, err := f.Append(nil, protocol.Version1)
+		require.NoError(t, err)
+		_, _, err = parser.ParseType(b, protocol.Encryption1RTT)
+		checkFrameUnsupported(t, err, uint64(FrameTypeAckFrequency))
+	})
+
+	t.Run("IMMEDIATE_ACK", func(t *testing.T) {
+		f := &ImmediateAckFrame{}
+		b, err := f.Append(nil, protocol.Version1)
+		require.NoError(t, err)
+		_, _, err = parser.ParseType(b, protocol.Encryption1RTT)
+		checkFrameUnsupported(t, err, uint64(FrameTypeImmediateAck))
+	})
 }
 
 func TestFrameParserInvalidFrameType(t *testing.T) {
diff --git a/internal/wire/immediate_ack_frame_test.go b/internal/wire/immediate_ack_frame_test.go
new file mode 100644
index 00000000..af045a86
--- /dev/null
+++ b/internal/wire/immediate_ack_frame_test.go
@@ -0,0 +1,22 @@
+package wire
+
+import (
+	"testing"
+
+	"github.com/quic-go/quic-go/internal/protocol"
+	"github.com/quic-go/quic-go/quicvarint"
+	"github.com/stretchr/testify/require"
+)
+
+func TestImmediateAckFrame(t *testing.T) {
+	frame := ImmediateAckFrame{}
+	b, err := frame.Append(nil, protocol.Version1)
+	require.NoError(t, err)
+
+	val, l, err := quicvarint.Parse(b)
+	require.NoError(t, err)
+	require.Equal(t, uint64(FrameTypeImmediateAck), val)
+	require.Equal(t, len(b), l)
+
+	require.Len(t, b, int(frame.Length(protocol.Version1)))
+}