Repository: valyala/fasthttp
Pull request #1999 (2025-04-28 06:28:06)

feat: move user values to Request structure
Moving user values to Request structure is helpful to propagate context through many abstraction layers (e.g. for tracing/custom middlewares on client side).

Now it's impossible (or VERY awkward) to implement telemetry for client, since `Client.Do` have only request and response arguments.

It solves #940 and many other issues.


Patch:
diff --git a/http.go b/http.go
index c15df1b..c5eae20 100644
--- a/http.go
+++ b/http.go
@@ -45,7 +45,8 @@ type Request struct {
 	multipartForm         *multipart.Form
 	multipartFormBoundary string
 
-	postArgs Args
+	postArgs   Args
+	userValues userData
 
 	bodyRaw []byte
 
@@ -1119,6 +1120,7 @@ func readMultipartForm(r io.Reader, boundary string, size, maxInMemoryFileSize i
 
 // Reset clears request contents.
 func (req *Request) Reset() {
+	req.userValues.Reset() // it should be at the top, since some values might implement io.Closer interface
 	if requestBodyPoolSizeLimit >= 0 && req.body != nil {
 		req.ReleaseBody(requestBodyPoolSizeLimit)
 	}
@@ -1486,8 +1488,12 @@ func (resp *Response) ReadBody(r *bufio.Reader, maxBodySize int) (err error) {
 			bodyBuf.B, err = readBodyChunked(r, maxBodySize, bodyBuf.B)
 		}
 	default:
-		bodyBuf.B, err = readBodyIdentity(r, maxBodySize, bodyBuf.B)
-		resp.Header.SetContentLength(len(bodyBuf.B))
+		if resp.StreamBody {
+			resp.bodyStream = acquireRequestStream(bodyBuf, r, &resp.Header)
+		} else {
+			bodyBuf.B, err = readBodyIdentity(r, maxBodySize, bodyBuf.B)
+			resp.Header.SetContentLength(len(bodyBuf.B))
+		}
 	}
 	if err == nil && resp.StreamBody && resp.bodyStream == nil {
 		resp.bodyStream = bytes.NewReader(bodyBuf.B)
@@ -2136,6 +2142,84 @@ func (resp *Response) String() string {
 	return getHTTPString(resp)
 }
 
+// SetUserValue stores the given value (arbitrary object)
+// under the given key in Request.
+//
+// The value stored in Request may be obtained by UserValue*.
+//
+// This functionality may be useful for passing arbitrary values between
+// functions involved in request processing.
+//
+// All the values are removed from Request after returning from the top
+// RequestHandler. Additionally, Close method is called on each value
+// implementing io.Closer before removing the value from Request.
+func (req *Request) SetUserValue(key, value any) {
+	req.userValues.Set(key, value)
+}
+
+// SetUserValueBytes stores the given value (arbitrary object)
+// under the given key in Request.
+//
+// The value stored in Request may be obtained by UserValue*.
+//
+// This functionality may be useful for passing arbitrary values between
+// functions involved in request processing.
+//
+// All the values stored in Request are deleted after returning from RequestHandler.
+func (req *Request) SetUserValueBytes(key []byte, value any) {
+	req.userValues.SetBytes(key, value)
+}
+
+// UserValue returns the value stored via SetUserValue* under the given key.
+func (req *Request) UserValue(key any) any {
+	return req.userValues.Get(key)
+}
+
+// UserValueBytes returns the value stored via SetUserValue*
+// under the given key.
+func (req *Request) UserValueBytes(key []byte) any {
+	return req.userValues.GetBytes(key)
+}
+
+// VisitUserValues calls visitor for each existing userValue with a key that is a string or []byte.
+//
+// visitor must not retain references to key and value after returning.
+// Make key and/or value copies if you need storing them after returning.
+func (req *Request) VisitUserValues(visitor func([]byte, any)) {
+	for i, n := 0, len(req.userValues); i < n; i++ {
+		kv := &req.userValues[i]
+		if _, ok := kv.key.(string); ok {
+			visitor(s2b(kv.key.(string)), kv.value)
+		}
+	}
+}
+
+// VisitUserValuesAll calls visitor for each existing userValue.
+//
+// visitor must not retain references to key and value after returning.
+// Make key and/or value copies if you need storing them after returning.
+func (req *Request) VisitUserValuesAll(visitor func(any, any)) {
+	for i, n := 0, len(req.userValues); i < n; i++ {
+		kv := &req.userValues[i]
+		visitor(kv.key, kv.value)
+	}
+}
+
+// ResetUserValues allows to reset user values from Request Context.
+func (req *Request) ResetUserValues() {
+	req.userValues.Reset()
+}
+
+// RemoveUserValue removes the given key and the value under it in Request.
+func (req *Request) RemoveUserValue(key any) {
+	req.userValues.Remove(key)
+}
+
+// RemoveUserValueBytes removes the given key and the value under it in Request.
+func (req *Request) RemoveUserValueBytes(key []byte) {
+	req.userValues.RemoveBytes(key)
+}
+
 func getHTTPString(hw httpWriter) string {
 	w := bytebufferpool.Get()
 	defer bytebufferpool.Put(w)
diff --git a/server.go b/server.go
index e1ae800..357544f 100644
--- a/server.go
+++ b/server.go
@@ -611,8 +611,6 @@ type RequestCtx struct {
 	formValueFunc FormValueFunc
 	fbr           firstByteReader
 
-	userValues userData
-
 	// Incoming request.
 	//
 	// Copying Request by value is forbidden. Use pointer to Request instead.
@@ -743,42 +741,42 @@ func (ctx *RequestCtx) Hijacked() bool {
 }
 
 // SetUserValue stores the given value (arbitrary object)
-// under the given key in ctx.
+// under the given key in Request.
 //
-// The value stored in ctx may be obtained by UserValue*.
+// The value stored in Request may be obtained by UserValue*.
 //
 // This functionality may be useful for passing arbitrary values between
 // functions involved in request processing.
 //
-// All the values are removed from ctx after returning from the top
+// All the values are removed from Request after returning from the top
 // RequestHandler. Additionally, Close method is called on each value
-// implementing io.Closer before removing the value from ctx.
+// implementing io.Closer before removing the value from Request.
 func (ctx *RequestCtx) SetUserValue(key, value any) {
-	ctx.userValues.Set(key, value)
+	ctx.Request.SetUserValue(key, value)
 }
 
 // SetUserValueBytes stores the given value (arbitrary object)
-// under the given key in ctx.
+// under the given key in Request.
 //
-// The value stored in ctx may be obtained by UserValue*.
+// The value stored in Request may be obtained by UserValue*.
 //
 // This functionality may be useful for passing arbitrary values between
 // functions involved in request processing.
 //
-// All the values stored in ctx are deleted after returning from RequestHandler.
+// All the values stored in Request are deleted after returning from RequestHandler.
 func (ctx *RequestCtx) SetUserValueBytes(key []byte, value any) {
-	ctx.userValues.SetBytes(key, value)
+	ctx.Request.SetUserValueBytes(key, value)
 }
 
 // UserValue returns the value stored via SetUserValue* under the given key.
 func (ctx *RequestCtx) UserValue(key any) any {
-	return ctx.userValues.Get(key)
+	return ctx.Request.UserValue(key)
 }
 
 // UserValueBytes returns the value stored via SetUserValue*
 // under the given key.
 func (ctx *RequestCtx) UserValueBytes(key []byte) any {
-	return ctx.userValues.GetBytes(key)
+	return ctx.Request.UserValueBytes(key)
 }
 
 // VisitUserValues calls visitor for each existing userValue with a key that is a string or []byte.
@@ -786,12 +784,7 @@ func (ctx *RequestCtx) UserValueBytes(key []byte) any {
 // visitor must not retain references to key and value after returning.
 // Make key and/or value copies if you need storing them after returning.
 func (ctx *RequestCtx) VisitUserValues(visitor func([]byte, any)) {
-	for i, n := 0, len(ctx.userValues); i < n; i++ {
-		kv := &ctx.userValues[i]
-		if _, ok := kv.key.(string); ok {
-			visitor(s2b(kv.key.(string)), kv.value)
-		}
-	}
+	ctx.Request.VisitUserValues(visitor)
 }
 
 // VisitUserValuesAll calls visitor for each existing userValue.
@@ -799,25 +792,22 @@ func (ctx *RequestCtx) VisitUserValues(visitor func([]byte, any)) {
 // visitor must not retain references to key and value after returning.
 // Make key and/or value copies if you need storing them after returning.
 func (ctx *RequestCtx) VisitUserValuesAll(visitor func(any, any)) {
-	for i, n := 0, len(ctx.userValues); i < n; i++ {
-		kv := &ctx.userValues[i]
-		visitor(kv.key, kv.value)
-	}
+	ctx.Request.VisitUserValuesAll(visitor)
 }
 
-// ResetUserValues allows to reset user values from Request Context.
+// ResetUserValues allows to reset user values from Request.
 func (ctx *RequestCtx) ResetUserValues() {
-	ctx.userValues.Reset()
+	ctx.Request.ResetUserValues()
 }
 
-// RemoveUserValue removes the given key and the value under it in ctx.
+// RemoveUserValue removes the given key and the value under it in Request.
 func (ctx *RequestCtx) RemoveUserValue(key any) {
-	ctx.userValues.Remove(key)
+	ctx.Request.RemoveUserValue(key)
 }
 
-// RemoveUserValueBytes removes the given key and the value under it in ctx.
+// RemoveUserValueBytes removes the given key and the value under it in Request.
 func (ctx *RequestCtx) RemoveUserValueBytes(key []byte) {
-	ctx.userValues.RemoveBytes(key)
+	ctx.Request.RemoveUserValueBytes(key)
 }
 
 type connTLSer interface {
@@ -873,7 +863,6 @@ func (ctx *RequestCtx) Conn() net.Conn {
 }
 
 func (ctx *RequestCtx) reset() {
-	ctx.userValues.Reset()
 	ctx.Request.Reset()
 	ctx.Response.Reset()
 	ctx.fbr.reset()
@@ -2565,7 +2554,6 @@ func (s *Server) serveConn(c net.Conn) (err error) {
 		}
 
 		s.setState(c, StateIdle)
-		ctx.userValues.Reset()
 		ctx.Request.Reset()
 		ctx.Response.Reset()
 
diff --git a/streaming.go b/streaming.go
index 6ebccf1..cc0f2d6 100644
--- a/streaming.go
+++ b/streaming.go
@@ -74,7 +74,7 @@ func (rs *requestStream) Read(p []byte) (int, error) {
 		return n, err
 	}
 	left := rs.header.ContentLength() - rs.totalBytesRead
-	if len(p) > left {
+	if left > 0 && len(p) > left {
 		p = p[:left]
 	}
 	n, err = rs.reader.Read(p)


Test patch:
diff --git a/http_test.go b/http_test.go
index 8ec2462..986c269 100644
--- a/http_test.go
+++ b/http_test.go
@@ -3057,6 +3057,31 @@ func TestResponseBodyStream(t *testing.T) {
 				t.Fatalf("unexpected body content, got: %#v, want: %#v", string(content), "0123456789")
 			}
 		})
+
+		t.Run("identity", func(t *testing.T) {
+			t.Parallel()
+
+			client := Client{StreamResponseBody: true, DisablePathNormalizing: true}
+			resp := AcquireResponse()
+			request := AcquireRequest()
+			request.SetRequestURI(server.URL + "?400BadRequest")
+			if err := client.Do(request, resp); err != nil {
+				t.Fatal(err)
+			}
+			stream := resp.BodyStream()
+			defer func() {
+				if err := resp.CloseBodyStream(); err != nil {
+					t.Fatalf("close stream err: %v", err)
+				}
+			}()
+			content, err := io.ReadAll(stream)
+			if err != nil {
+				t.Fatal(err)
+			}
+			if string(content) != "400 Bad Request" {
+				t.Fatalf("unexpected body content, got: %#v, want: %#v", string(content), "400 Bad Request")
+			}
+		})
 	})
 }
 
diff --git a/server_test.go b/server_test.go
index 9dcc5ad..91be278 100644
--- a/server_test.go
+++ b/server_test.go
@@ -1819,8 +1819,8 @@ func TestRequestCtxUserValue(t *testing.T) {
 			t.Fatalf("unexpected value obtained from VisitUserValues for key: %q, expecting: %#v but got: %#v", key, v, value)
 		}
 	})
-	if len(ctx.userValues) != vlen {
-		t.Fatalf("the length of user values returned from VisitUserValues is not equal to the length of the userValues, expecting: %d but got: %d", len(ctx.userValues), vlen)
+	if len(ctx.Request.userValues) != vlen {
+		t.Fatalf("the length of user values returned from VisitUserValues is not equal to the length of the userValues, expecting: %d but got: %d", len(ctx.Request.userValues), vlen)
 	}
 
 	ctx.ResetUserValues()