Repository: valyala/fasthttp (go)

Issue:
Proposal: Add iter.Seq2 iterator to Args, ResponseHeader, RequestHeader Visit methods
With Go 1.23 introducing the new iter.Seq and iter.Seq2 iteration patterns, it would be great to provide Go 1.23-native iteration support for some of the existing VisitAll-style methods.

We could add iter.Seq versions for the following methods:

+ Args.VisitAll
+ ResponseHeader.VisitAll
+ ResponseHeader.VisitAllTrailer
+ ResponseHeader.VisitAllCookie
+ RequestHeader.VisitAll
+ RequestHeader.VisitAllTrailer
+ RequestHeader.VisitAllCookie

These iterators would support `break` and `continue` control flow, offering more flexibility compared to the original callback-based implementation.

Pull request:
feat: Add iter.Seq2 iterator
Resolves #2010

Create iterator for 
+ `Args.VisitAll` → `Args.All`
+ `ResponseHeader.VisitAll` → `ResponseHeader.All`
+ `ResponseHeader.VisitAllTrailer` → `ResponseHeader.Trailers`
+ `ResponseHeader.VisitAllCookie` → `ResponseHeader.Cookies`
+ `RequestHeader.VisitAll` → `RequestHeader.All`
+ `RequestHeader.VisitAllTrailer` → `RequestHeader.Trailers`
+ `RequestHeader.VisitAllCookie` → `RequestHeader.Cookies`
+ `RequestHeader.VisitAllInOrder` → `RequestHeader.AllInOrder`

Patch:
diff --git a/args.go b/args.go
index 254c5d5..938e62e 100644
--- a/args.go
+++ b/args.go
@@ -4,6 +4,7 @@ import (
 	"bytes"
 	"errors"
 	"io"
+	"iter"
 	"sort"
 	"sync"
 )
@@ -64,12 +65,31 @@ func (a *Args) CopyTo(dst *Args) {
 	dst.args = copyArgs(dst.args, a.args)
 }
 
+// All returns an iterator over key-value pairs from args.
+//
+// The key and value may invalid outside the iteration loop.
+// Make copies if you need to use them after the loop ends.
+func (a *Args) All() iter.Seq2[[]byte, []byte] {
+	return func(yield func([]byte, []byte) bool) {
+		for i := range a.args {
+			if !yield(a.args[i].key, a.args[i].value) {
+				break
+			}
+		}
+	}
+}
+
 // VisitAll calls f for each existing arg.
 //
 // f must not retain references to key and value after returning.
 // Make key and/or value copies if you need storing them after returning.
+//
+// Deprecated: Use All instead.
 func (a *Args) VisitAll(f func(key, value []byte)) {
-	visitArgs(a.args, f)
+	a.All()(func(key, value []byte) bool {
+		f(key, value)
+		return true
+	})
 }
 
 // Len returns the number of query args.
@@ -256,11 +276,11 @@ func (a *Args) PeekBytes(key []byte) []byte {
 // PeekMulti returns all the arg values for the given key.
 func (a *Args) PeekMulti(key string) [][]byte {
 	var values [][]byte
-	a.VisitAll(func(k, v []byte) {
+	for k, v := range a.All() {
 		if string(k) == key {
 			values = append(values, v)
 		}
-	})
+	}
 	return values
 }
 
@@ -349,13 +369,6 @@ func (a *Args) GetBool(key string) bool {
 	}
 }
 
-func visitArgs(args []argsKV, f func(k, v []byte)) {
-	for i, n := 0, len(args); i < n; i++ {
-		kv := &args[i]
-		f(kv.key, kv.value)
-	}
-}
-
 func copyArgs(dst, src []argsKV) []argsKV {
 	if cap(dst) < len(src) {
 		tmp := make([]argsKV, len(src))
diff --git a/fasthttpadaptor/request.go b/fasthttpadaptor/request.go
index 3526666..6213f28 100644
--- a/fasthttpadaptor/request.go
+++ b/fasthttpadaptor/request.go
@@ -51,7 +51,7 @@ func ConvertRequest(ctx *fasthttp.RequestCtx, r *http.Request, forServer bool) e
 		}
 	}
 
-	ctx.Request.Header.VisitAll(func(k, v []byte) {
+	for k, v := range ctx.Request.Header.All() {
 		sk := b2s(k)
 		sv := b2s(v)
 
@@ -64,7 +64,7 @@ func ConvertRequest(ctx *fasthttp.RequestCtx, r *http.Request, forServer bool) e
 			}
 			r.Header.Set(sk, sv)
 		}
-	})
+	}
 
 	return nil
 }
diff --git a/header.go b/header.go
index d2850b1..178acf7 100644
--- a/header.go
+++ b/header.go
@@ -6,6 +6,7 @@ import (
 	"errors"
 	"fmt"
 	"io"
+	"iter"
 	"sync"
 	"sync/atomic"
 	"time"
@@ -934,7 +935,9 @@ func (h *RequestHeader) HasAcceptEncodingBytes(acceptEncoding []byte) bool {
 // i.e. the number of times f is called in VisitAll.
 func (h *ResponseHeader) Len() int {
 	n := 0
-	h.VisitAll(func(_, _ []byte) { n++ })
+	for range h.All() {
+		n++
+	}
 	return n
 }
 
@@ -942,7 +945,9 @@ func (h *ResponseHeader) Len() int {
 // i.e. the number of times f is called in VisitAll.
 func (h *RequestHeader) Len() int {
 	n := 0
-	h.VisitAll(func(_, _ []byte) { n++ })
+	for range h.All() {
+		n++
+	}
 	return n
 }
 
@@ -1143,55 +1148,129 @@ func (h *RequestHeader) CopyTo(dst *RequestHeader) {
 	dst.rawHeaders = append(dst.rawHeaders, h.rawHeaders...)
 }
 
+// All returns an iterator over key-value pairs in h.
+//
+// The key and value may invalid outside the iteration loop.
+// Copy key and/or value contents for each iteration if you need retaining
+// them.
+func (h *ResponseHeader) All() iter.Seq2[[]byte, []byte] {
+	return func(yield func([]byte, []byte) bool) {
+		if len(h.contentLengthBytes) > 0 && !yield(strContentLength, h.contentLengthBytes) {
+			return
+		}
+		if contentType := h.ContentType(); len(contentType) > 0 && !yield(strContentType, contentType) {
+			return
+		}
+
+		if contentEncoding := h.ContentEncoding(); len(contentEncoding) > 0 && !yield(strContentEncoding, contentEncoding) {
+			return
+		}
+
+		if server := h.Server(); len(server) > 0 && !yield(strServer, server) {
+			return
+		}
+
+		for i := range h.cookies {
+			if !yield(strSetCookie, h.cookies[i].value) {
+				return
+			}
+		}
+
+		if len(h.trailer) > 0 && !yield(strTrailer, appendTrailerBytes(nil, h.trailer, strCommaSpace)) {
+			return
+		}
+
+		for i := range h.h {
+			if !yield(h.h[i].key, h.h[i].value) {
+				return
+			}
+		}
+
+		if h.ConnectionClose() && !yield(strConnection, strClose) {
+			return
+		}
+	}
+}
+
 // VisitAll calls f for each header.
 //
 // f must not retain references to key and/or value after returning.
 // Copy key and/or value contents before returning if you need retaining them.
+//
+// Deprecated: Use All instead.
 func (h *ResponseHeader) VisitAll(f func(key, value []byte)) {
-	if len(h.contentLengthBytes) > 0 {
-		f(strContentLength, h.contentLengthBytes)
-	}
-	contentType := h.ContentType()
-	if len(contentType) > 0 {
-		f(strContentType, contentType)
-	}
-	contentEncoding := h.ContentEncoding()
-	if len(contentEncoding) > 0 {
-		f(strContentEncoding, contentEncoding)
-	}
-	server := h.Server()
-	if len(server) > 0 {
-		f(strServer, server)
-	}
-	if len(h.cookies) > 0 {
-		visitArgs(h.cookies, func(_, v []byte) {
-			f(strSetCookie, v)
-		})
-	}
-	if len(h.trailer) > 0 {
-		f(strTrailer, appendTrailerBytes(nil, h.trailer, strCommaSpace))
-	}
-	visitArgs(h.h, f)
-	if h.ConnectionClose() {
-		f(strConnection, strClose)
+	h.All()(func(key, value []byte) bool {
+		f(key, value)
+		return true
+	})
+}
+
+// Trailers returns an iterator over trailers in h.
+//
+// The value of trailer may invalid outside the iteration loop.
+func (h *ResponseHeader) Trailers() iter.Seq[[]byte] {
+	return func(yield func([]byte) bool) {
+		for i := range h.trailer {
+			if !yield(h.trailer[i]) {
+				break
+			}
+		}
 	}
 }
 
 // VisitAllTrailer calls f for each response Trailer.
 //
 // f must not retain references to value after returning.
+//
+// Deprecated: Use Trailers instead.
 func (h *ResponseHeader) VisitAllTrailer(f func(value []byte)) {
-	for i := range h.trailer {
-		f(h.trailer[i])
+	h.Trailers()(func(v []byte) bool {
+		f(v)
+		return true
+	})
+}
+
+// Trailers returns an iterator over trailers in h.
+//
+// The value of trailer may invalid outside the iteration loop.
+func (h *RequestHeader) Trailers() iter.Seq[[]byte] {
+	return func(yield func([]byte) bool) {
+		for i := range h.trailer {
+			if !yield(h.trailer[i]) {
+				break
+			}
+		}
 	}
 }
 
 // VisitAllTrailer calls f for each request Trailer.
 //
 // f must not retain references to value after returning.
+//
+// Deprecated: Use Trailers instead.
 func (h *RequestHeader) VisitAllTrailer(f func(value []byte)) {
-	for i := range h.trailer {
-		f(h.trailer[i])
+	h.Trailers()(func(v []byte) bool {
+		f(v)
+		return true
+	})
+}
+
+// Cookies returns an iterator over key-value paired response cookie in h.
+//
+// Cookie name is passed in key and the whole Set-Cookie header value
+// is passed in value for each iteration. Value may be parsed with
+// Cookie.ParseBytes().
+//
+// The key and value may invalid outside the iteration loop.
+// Copy key and/or value contents for each iteration if you need retaining
+// them.
+func (h *ResponseHeader) Cookies() iter.Seq2[[]byte, []byte] {
+	return func(yield func([]byte, []byte) bool) {
+		for i := range h.cookies {
+			if !yield(h.cookies[i].key, h.cookies[i].value) {
+				break
+			}
+		}
 	}
 }
 
@@ -1202,16 +1281,89 @@ func (h *RequestHeader) VisitAllTrailer(f func(value []byte)) {
 // with Cookie.ParseBytes().
 //
 // f must not retain references to key and/or value after returning.
+//
+// Deprecated: Use Cookies instead.
 func (h *ResponseHeader) VisitAllCookie(f func(key, value []byte)) {
-	visitArgs(h.cookies, f)
+	h.Cookies()(func(key, value []byte) bool {
+		f(key, value)
+		return true
+	})
+}
+
+// Cookies returns an iterator over key-value pairs request cookie in h.
+//
+// The key and value may invalid outside the iteration loop.
+// Copy key and/or value contents for each iteration if you need retaining
+// them.
+func (h *RequestHeader) Cookies() iter.Seq2[[]byte, []byte] {
+	return func(yield func([]byte, []byte) bool) {
+		h.collectCookies()
+		for i := range h.cookies {
+			if !yield(h.cookies[i].key, h.cookies[i].value) {
+				break
+			}
+		}
+	}
 }
 
 // VisitAllCookie calls f for each request cookie.
 //
 // f must not retain references to key and/or value after returning.
+//
+// Deprecated: Use Cookies instead.
 func (h *RequestHeader) VisitAllCookie(f func(key, value []byte)) {
-	h.collectCookies()
-	visitArgs(h.cookies, f)
+	h.Cookies()(func(key, value []byte) bool {
+		f(key, value)
+		return true
+	})
+}
+
+// All returns an iterator over key-value pairs in h.
+//
+// The key and value may invalid outside the iteration loop.
+// Copy key and/or value contents for each iteration if you need retaining
+// them.
+//
+// To get the headers in order they were received use AllInOrder.
+func (h *RequestHeader) All() iter.Seq2[[]byte, []byte] {
+	return func(yield func([]byte, []byte) bool) {
+		if host := h.Host(); len(host) > 0 && !yield(strHost, host) {
+			return
+		}
+		if len(h.contentLengthBytes) > 0 && !yield(strContentLength, h.contentLengthBytes) {
+			return
+		}
+
+		if contentType := h.ContentType(); len(contentType) > 0 && !yield(strContentType, contentType) {
+			return
+		}
+
+		if userAgent := h.UserAgent(); len(userAgent) > 0 && !yield(strUserAgent, userAgent) {
+			return
+		}
+
+		if len(h.trailer) > 0 && !yield(strTrailer, appendTrailerBytes(nil, h.trailer, strCommaSpace)) {
+			return
+		}
+
+		h.collectCookies()
+
+		if len(h.cookies) > 0 {
+			h.bufV = appendRequestCookieBytes(h.bufV[:0], h.cookies)
+			if !yield(strCookie, h.bufV) {
+				return
+			}
+		}
+
+		for i := range h.h {
+			if !yield(h.h[i].key, h.h[i].value) {
+				return
+			}
+		}
+		if h.ConnectionClose() && !yield(strConnection, strClose) {
+			return
+		}
+	}
 }
 
 // VisitAll calls f for each header.
@@ -1220,34 +1372,36 @@ func (h *RequestHeader) VisitAllCookie(f func(key, value []byte)) {
 // Copy key and/or value contents before returning if you need retaining them.
 //
 // To get the headers in order they were received use VisitAllInOrder.
+//
+// Deprecated: Use All instead.
 func (h *RequestHeader) VisitAll(f func(key, value []byte)) {
-	host := h.Host()
-	if len(host) > 0 {
-		f(strHost, host)
-	}
-	if len(h.contentLengthBytes) > 0 {
-		f(strContentLength, h.contentLengthBytes)
-	}
-	contentType := h.ContentType()
-	if len(contentType) > 0 {
-		f(strContentType, contentType)
-	}
-	userAgent := h.UserAgent()
-	if len(userAgent) > 0 {
-		f(strUserAgent, userAgent)
-	}
-	if len(h.trailer) > 0 {
-		f(strTrailer, appendTrailerBytes(nil, h.trailer, strCommaSpace))
-	}
-
-	h.collectCookies()
-	if len(h.cookies) > 0 {
-		h.bufV = appendRequestCookieBytes(h.bufV[:0], h.cookies)
-		f(strCookie, h.bufV)
-	}
-	visitArgs(h.h, f)
-	if h.ConnectionClose() {
-		f(strConnection, strClose)
+	h.All()(func(key, value []byte) bool {
+		f(key, value)
+		return true
+	})
+}
+
+// AllInOrder returns an iterator over key-value pairs in h in the order they
+// were received.
+//
+// The key and value may invalid outside the iteration loop.
+// Copy key and/or value contents for each iteration if you need retaining
+// them.
+//
+// The returned iterator is slightly slower than All because it has to reparse
+// the raw headers to get the order.
+func (h *RequestHeader) AllInOrder() iter.Seq2[[]byte, []byte] {
+	return func(yield func([]byte, []byte) bool) {
+		var s headerScanner
+		s.b = h.rawHeaders
+		for s.next() {
+			normalizeHeaderKey(s.key, h.disableNormalizing || bytes.IndexByte(s.key, ' ') != -1)
+			if len(s.key) > 0 {
+				if !yield(s.key, s.value) {
+					break
+				}
+			}
+		}
 	}
 }
 
@@ -1258,15 +1412,13 @@ func (h *RequestHeader) VisitAll(f func(key, value []byte)) {
 //
 // This function is slightly slower than VisitAll because it has to reparse the
 // raw headers to get the order.
+//
+// Deprecated: Use AllInOrder instead.
 func (h *RequestHeader) VisitAllInOrder(f func(key, value []byte)) {
-	var s headerScanner
-	s.b = h.rawHeaders
-	for s.next() {
-		normalizeHeaderKey(s.key, h.disableNormalizing || bytes.IndexByte(s.key, ' ') != -1)
-		if len(s.key) > 0 {
-			f(s.key, s.value)
-		}
-	}
+	h.AllInOrder()(func(key, value []byte) bool {
+		f(key, value)
+		return true
+	})
 }
 
 // Del deletes header with the given key.


Test patch:
diff --git a/args_test.go b/args_test.go
index 9dff711..6d32245 100644
--- a/args_test.go
+++ b/args_test.go
@@ -68,7 +68,7 @@ func TestArgsAdd(t *testing.T) {
 	}
 
 	var barFound, bazFound, oneFound, emptyFound1, emptyFound2, baFound bool
-	a1.VisitAll(func(k, v []byte) {
+	for k, v := range a1.All() {
 		switch string(k) {
 		case "foo":
 			switch string(v) {
@@ -95,7 +95,7 @@ func TestArgsAdd(t *testing.T) {
 		default:
 			t.Fatalf("unexpected key found %q", k)
 		}
-	})
+	}
 	if !barFound || !bazFound || !oneFound || !emptyFound1 || !emptyFound2 || !baFound {
 		t.Fatalf("something is missing: %v, %v, %v, %v, %v, %v", barFound, bazFound, oneFound, emptyFound1, emptyFound2, baFound)
 	}
@@ -329,9 +329,9 @@ func TestArgsCopyTo(t *testing.T) {
 
 func testCopyTo(t *testing.T, a *Args) {
 	keys := make(map[string]struct{})
-	a.VisitAll(func(k, _ []byte) {
+	for k := range a.All() {
 		keys[string(k)] = struct{}{}
-	})
+	}
 
 	var b Args
 	a.CopyTo(&b)
@@ -340,12 +340,12 @@ func testCopyTo(t *testing.T, a *Args) {
 		t.Fatalf("ArgsCopyTo fail, a: \n%+v\nb: \n%+v\n", a, &b)
 	}
 
-	b.VisitAll(func(k, _ []byte) {
+	for k := range b.All() {
 		if _, ok := keys[string(k)]; !ok {
 			t.Fatalf("unexpected key %q after copying from %q", k, a.String())
 		}
 		delete(keys, string(k))
-	})
+	}
 	if len(keys) > 0 {
 		t.Fatalf("missing keys %#v after copying from %q", keys, a.String())
 	}
diff --git a/header_test.go b/header_test.go
index 3c3affa..088c08c 100644
--- a/header_test.go
+++ b/header_test.go
@@ -171,9 +171,9 @@ func TestResponseHeaderMultiLineName(t *testing.T) {
 	header := new(ResponseHeader)
 	if _, err := header.parse([]byte(s)); err != errInvalidName {
 		m := make(map[string]string)
-		header.VisitAll(func(key, value []byte) {
-			m[string(key)] = string(value)
-		})
+		for k, v := range header.All() {
+			m[string(k)] = string(v)
+		}
 		t.Errorf("expected error, got %q (%v)", m, err)
 	}
 
@@ -530,7 +530,7 @@ func TestResponseHeaderAdd(t *testing.T) {
 		t.Fatalf("unexpected header len %d. Expecting 12", h.Len())
 	}
 
-	h.VisitAll(func(k, v []byte) {
+	for k, v := range h.All() {
 		switch string(k) {
 		case "Aaa", "Foo-Bar", "Content-Type":
 			if _, ok := m[string(v)]; !ok {
@@ -540,7 +540,7 @@ func TestResponseHeaderAdd(t *testing.T) {
 		default:
 			t.Fatalf("unexpected key found: %q", k)
 		}
-	})
+	}
 	if len(m) > 0 {
 		t.Fatalf("%d headers are missed", len(m))
 	}
@@ -552,14 +552,14 @@ func TestResponseHeaderAdd(t *testing.T) {
 		t.Fatalf("unexpected error: %v", err)
 	}
 
-	h.VisitAll(func(k, v []byte) {
+	for k, v := range h.All() {
 		switch string(k) {
 		case "Aaa", "Foo-Bar", "Content-Type":
 			m[string(v)] = struct{}{}
 		default:
 			t.Fatalf("unexpected key found: %q", k)
 		}
-	})
+	}
 	if len(m) != 12 {
 		t.Fatalf("unexpected number of headers: %d. Expecting 12", len(m))
 	}
@@ -583,7 +583,7 @@ func TestRequestHeaderAdd(t *testing.T) {
 		t.Fatalf("unexpected header len %d. Expecting 12", h.Len())
 	}
 
-	h.VisitAll(func(k, v []byte) {
+	for k, v := range h.All() {
 		switch string(k) {
 		case "Aaa", "Foo-Bar", "User-Agent":
 			if _, ok := m[string(v)]; !ok {
@@ -593,7 +593,7 @@ func TestRequestHeaderAdd(t *testing.T) {
 		default:
 			t.Fatalf("unexpected key found: %q", k)
 		}
-	})
+	}
 	if len(m) > 0 {
 		t.Fatalf("%d headers are missed", len(m))
 	}
@@ -605,14 +605,14 @@ func TestRequestHeaderAdd(t *testing.T) {
 		t.Fatalf("unexpected error: %v", err)
 	}
 
-	h.VisitAll(func(k, v []byte) {
+	for k, v := range h.All() {
 		switch string(k) {
 		case "Aaa", "Foo-Bar", "User-Agent":
 			m[string(v)] = struct{}{}
 		default:
 			t.Fatalf("unexpected key found: %q", k)
 		}
-	})
+	}
 	if len(m) != 12 {
 		t.Fatalf("unexpected number of headers: %d. Expecting 12", len(m))
 	}
@@ -1933,7 +1933,7 @@ func TestResponseHeaderCookie(t *testing.T) {
 		t.Fatalf("unexpected cookie\n%v\nExpected\n%v\n", &c, &expectedC2)
 	}
 
-	h.VisitAllCookie(func(key, value []byte) {
+	for key, value := range h.Cookies() {
 		var cc Cookie
 		if err := cc.ParseBytes(value); err != nil {
 			t.Fatal(err)
@@ -1953,7 +1953,7 @@ func TestResponseHeaderCookie(t *testing.T) {
 		default:
 			t.Fatalf("unexpected cookie key %q", key)
 		}
-	})
+	}
 
 	w := &bytes.Buffer{}
 	bw := bufio.NewWriter(w)
@@ -2094,11 +2094,12 @@ func TestResponseHeaderCookieIssue4(t *testing.T) {
 		t.Fatalf("Unexpected Set-Cookie header %q. Expected %q", h.Peek(HeaderSetCookie), "foo=bar")
 	}
 	cookieSeen := false
-	h.VisitAll(func(key, _ []byte) {
+	for key := range h.All() {
 		if string(key) == HeaderSetCookie {
 			cookieSeen = true
+			break
 		}
-	})
+	}
 	if !cookieSeen {
 		t.Fatalf("Set-Cookie not present in VisitAll")
 	}
@@ -2114,11 +2115,12 @@ func TestResponseHeaderCookieIssue4(t *testing.T) {
 		t.Fatalf("Unexpected Set-Cookie header %q. Expected %q", h.Peek(HeaderSetCookie), "foo=bar")
 	}
 	cookieSeen = false
-	h.VisitAll(func(key, _ []byte) {
+	for key := range h.All() {
 		if string(key) == HeaderSetCookie {
 			cookieSeen = true
+			break
 		}
-	})
+	}
 	if !cookieSeen {
 		t.Fatalf("Set-Cookie not present in VisitAll")
 	}
@@ -2137,11 +2139,12 @@ func TestRequestHeaderCookieIssue313(t *testing.T) {
 		t.Fatalf("Unexpected Cookie header %q. Expected %q", h.Peek(HeaderCookie), "foo=bar")
 	}
 	cookieSeen := false
-	h.VisitAll(func(key, _ []byte) {
+	for key := range h.All() {
 		if string(key) == HeaderCookie {
 			cookieSeen = true
+			break
 		}
-	})
+	}
 	if !cookieSeen {
 		t.Fatalf("Cookie not present in VisitAll")
 	}
@@ -2154,11 +2157,12 @@ func TestRequestHeaderCookieIssue313(t *testing.T) {
 		t.Fatalf("Unexpected Cookie header %q. Expected %q", h.Peek(HeaderCookie), "foo=bar")
 	}
 	cookieSeen = false
-	h.VisitAll(func(key, _ []byte) {
+	for key := range h.All() {
 		if string(key) == HeaderCookie {
 			cookieSeen = true
+			break
 		}
-	})
+	}
 	if !cookieSeen {
 		t.Fatalf("Cookie not present in VisitAll")
 	}