Repository: valyala/fasthttp
Pull request #778 (2020-04-11 00:23:48)

Add configuration to not pre-read multipart form data
Some servers want to treat the multipart form data as a binary blob, or at least not read/parse it until other decisions are made about the request.

Markmerc: > I think the term `pre-read` is a bit wrong (also in the original code). Can you change it to `pre-parse`?

Fixed

Patch:
diff --git a/http.go b/http.go
index fbb54c7..3869b0a 100644
--- a/http.go
+++ b/http.go
@@ -937,10 +937,10 @@ func (req *Request) ReadLimitBody(r *bufio.Reader, maxBodySize int) error {
 		return err
 	}
 
-	return req.readLimitBody(r, maxBodySize, false)
+	return req.readLimitBody(r, maxBodySize, false, true)
 }
 
-func (req *Request) readLimitBody(r *bufio.Reader, maxBodySize int, getOnly bool) error {
+func (req *Request) readLimitBody(r *bufio.Reader, maxBodySize int, getOnly bool, preParseMultipartForm bool) error {
 	// Do not reset the request here - the caller must reset it before
 	// calling this method.
 
@@ -955,7 +955,7 @@ func (req *Request) readLimitBody(r *bufio.Reader, maxBodySize int, getOnly bool
 		return nil
 	}
 
-	return req.ContinueReadBody(r, maxBodySize)
+	return req.ContinueReadBody(r, maxBodySize, preParseMultipartForm)
 }
 
 // MayContinue returns true if the request contains
@@ -979,7 +979,7 @@ func (req *Request) MayContinue() bool {
 //
 // If maxBodySize > 0 and the body size exceeds maxBodySize,
 // then ErrBodyTooLarge is returned.
-func (req *Request) ContinueReadBody(r *bufio.Reader, maxBodySize int) error {
+func (req *Request) ContinueReadBody(r *bufio.Reader, maxBodySize int, preParseMultipartForm ...bool) error {
 	var err error
 	contentLength := req.Header.realContentLength()
 	if contentLength > 0 {
@@ -987,16 +987,18 @@ func (req *Request) ContinueReadBody(r *bufio.Reader, maxBodySize int) error {
 			return ErrBodyTooLarge
 		}
 
-		// Pre-read multipart form data of known length.
-		// This way we limit memory usage for large file uploads, since their contents
-		// is streamed into temporary files if file size exceeds defaultMaxInMemoryFileSize.
-		req.multipartFormBoundary = string(req.Header.MultipartFormBoundary())
-		if len(req.multipartFormBoundary) > 0 && len(req.Header.peek(strContentEncoding)) == 0 {
-			req.multipartForm, err = readMultipartForm(r, req.multipartFormBoundary, contentLength, defaultMaxInMemoryFileSize)
-			if err != nil {
-				req.Reset()
+		if len(preParseMultipartForm) == 0 || preParseMultipartForm[0] {
+			// Pre-read multipart form data of known length.
+			// This way we limit memory usage for large file uploads, since their contents
+			// is streamed into temporary files if file size exceeds defaultMaxInMemoryFileSize.
+			req.multipartFormBoundary = string(req.Header.MultipartFormBoundary())
+			if len(req.multipartFormBoundary) > 0 && len(req.Header.peek(strContentEncoding)) == 0 {
+				req.multipartForm, err = readMultipartForm(r, req.multipartFormBoundary, contentLength, defaultMaxInMemoryFileSize)
+				if err != nil {
+					req.Reset()
+				}
+				return err
 			}
-			return err
 		}
 	}
 
diff --git a/server.go b/server.go
index 6b66c29..43a4507 100644
--- a/server.go
+++ b/server.go
@@ -280,6 +280,14 @@ type Server struct {
 	// Server accepts all the requests by default.
 	GetOnly bool
 
+	// Will not pre parse Multipart Form data if set to true.
+	//
+	// This option is useful for servers that desire to treat
+	// multipart form data as a binary blob, or choose when to parse the data.
+	//
+	// Server pre parses multipart form data by default.
+	DisablePreParseMultipartForm bool
+
 	// Logs all errors, including the most frequent
 	// 'connection reset by peer', 'broken pipe' and 'connection timeout'
 	// errors. Such errors are common in production serving real-world
@@ -1988,7 +1996,7 @@ func (s *Server) serveConn(c net.Conn) (err error) {
 					}
 				}
 				//read body
-				err = ctx.Request.readLimitBody(br, maxRequestBodySize, s.GetOnly)
+				err = ctx.Request.readLimitBody(br, maxRequestBodySize, s.GetOnly, !s.DisablePreParseMultipartForm)
 			}
 			if err == nil {
 				// If we read any bytes off the wire, we're active.
@@ -2047,7 +2055,7 @@ func (s *Server) serveConn(c net.Conn) (err error) {
 			if br == nil {
 				br = acquireReader(ctx)
 			}
-			err = ctx.Request.ContinueReadBody(br, maxRequestBodySize)
+			err = ctx.Request.ContinueReadBody(br, maxRequestBodySize, !s.DisablePreParseMultipartForm)
 			if (s.ReduceMemoryUsage && br.Buffered() == 0) || err != nil {
 				releaseReader(s, br)
 				br = nil


Test patch:
diff --git a/http_test.go b/http_test.go
index 2c9be2a..3173d2d 100644
--- a/http_test.go
+++ b/http_test.go
@@ -869,7 +869,7 @@ func TestRequestContinueReadBody(t *testing.T) {
 		t.Fatalf("MayContinue must return true")
 	}
 
-	if err := r.ContinueReadBody(br, 0); err != nil {
+	if err := r.ContinueReadBody(br, 0, true); err != nil {
 		t.Fatalf("error when reading request body: %s", err)
 	}
 	body := r.Body()
@@ -886,6 +886,48 @@ func TestRequestContinueReadBody(t *testing.T) {
 	}
 }
 
+func TestRequestContinueReadBodyDisablePrereadMultipartForm(t *testing.T) {
+	t.Parallel()
+
+	var w bytes.Buffer
+	mw := multipart.NewWriter(&w)
+	for i := 0; i < 10; i++ {
+		k := fmt.Sprintf("key_%d", i)
+		v := fmt.Sprintf("value_%d", i)
+		if err := mw.WriteField(k, v); err != nil {
+			t.Fatalf("unexpected error: %s", err)
+		}
+	}
+	boundary := mw.Boundary()
+	if err := mw.Close(); err != nil {
+		t.Fatalf("unexpected error: %s", err)
+	}
+	formData := w.Bytes()
+
+	s := fmt.Sprintf("POST / HTTP/1.1\r\nHost: aaa\r\nContent-Type: multipart/form-data; boundary=%s\r\nContent-Length: %d\r\n\r\n%s",
+		boundary, len(formData), formData)
+	br := bufio.NewReader(bytes.NewBufferString(s))
+
+	var r Request
+
+	if err := r.Header.Read(br); err != nil {
+		t.Fatalf("unexpected error reading headers: %s", err)
+	}
+
+	if err := r.readLimitBody(br, 10000, false, false); err != nil {
+		t.Fatalf("unexpected error reading body: %s", err)
+	}
+
+	if r.multipartForm != nil {
+		t.Fatalf("The multipartForm of the Request must be nil")
+	}
+
+	if string(formData) != string(r.Body()) {
+		t.Fatalf("The body given must equal the body in the Request")
+	}
+
+}
+
 func TestRequestMayContinue(t *testing.T) {
 	t.Parallel()