Repository: valyala/fasthttp
Pull request #1311 (2022-06-04 22:19:04)

Response.ContentEncoding(): store as field and avoid using Header.SetCanonical()
The CE is not so often used for plain APIs responses and even not so often used for static files and on the fly compression.
But still it should be checked each time so instead of peek headers map it will be faster to check a field.
Also having a dedicated field getter and setter simplifies code.


Patch:
diff --git a/fs.go b/fs.go
index 90bbf12..4bbb3fd 100644
--- a/fs.go
+++ b/fs.go
@@ -898,16 +898,16 @@ func (h *fsHandler) handleRequest(ctx *RequestCtx) {
 	hdr := &ctx.Response.Header
 	if ff.compressed {
 		if fileEncoding == "br" {
-			hdr.SetCanonical(strContentEncoding, strBr)
+			hdr.SetContentEncodingBytes(strBr)
 		} else if fileEncoding == "gzip" {
-			hdr.SetCanonical(strContentEncoding, strGzip)
+			hdr.SetContentEncodingBytes(strGzip)
 		}
 	}
 
 	statusCode := StatusOK
 	contentLength := ff.contentLength
 	if h.acceptByteRange {
-		hdr.SetCanonical(strAcceptRanges, strBytes)
+		hdr.setNonSpecial(strAcceptRanges, strBytes)
 		if len(byteRange) > 0 {
 			startPos, endPos, err := ParseByteRange(byteRange, contentLength)
 			if err != nil {
@@ -930,7 +930,7 @@ func (h *fsHandler) handleRequest(ctx *RequestCtx) {
 		}
 	}
 
-	hdr.SetCanonical(strLastModified, ff.lastModifiedStr)
+	hdr.setNonSpecial(strLastModified, ff.lastModifiedStr)
 	if !ctx.IsHead() {
 		ctx.SetBodyStream(r, contentLength)
 	} else {
diff --git a/header.go b/header.go
index 08f5912..fc61ae2 100644
--- a/header.go
+++ b/header.go
@@ -39,8 +39,9 @@ type ResponseHeader struct {
 	contentLengthBytes    []byte
 	secureErrorLogMessage bool
 
-	contentType []byte
-	server      []byte
+	contentType     []byte
+	contentEncoding []byte
+	server          []byte
 
 	h       []argsKV
 	trailer []argsKV
@@ -103,7 +104,7 @@ func (h *ResponseHeader) SetContentRange(startPos, endPos, contentLength int) {
 	b = AppendUint(b, contentLength)
 	h.bufKV.value = b
 
-	h.SetCanonical(strContentRange, h.bufKV.value)
+	h.setNonSpecial(strContentRange, h.bufKV.value)
 }
 
 // SetByteRange sets 'Range: bytes=startPos-endPos' header.
@@ -125,7 +126,7 @@ func (h *RequestHeader) SetByteRange(startPos, endPos int) {
 	}
 	h.bufKV.value = b
 
-	h.SetCanonical(strRange, h.bufKV.value)
+	h.setNonSpecial(strRange, h.bufKV.value)
 }
 
 // StatusCode returns response status code.
@@ -167,7 +168,7 @@ func (h *ResponseHeader) SetProtocol(protocol []byte) {
 // SetLastModified sets 'Last-Modified' header to the given value.
 func (h *ResponseHeader) SetLastModified(t time.Time) {
 	h.bufKV.value = AppendHTTPDate(h.bufKV.value[:0], t)
-	h.SetCanonical(strLastModified, h.bufKV.value)
+	h.setNonSpecial(strLastModified, h.bufKV.value)
 }
 
 // ConnectionClose returns true if 'Connection: close' header is set.
@@ -325,6 +326,21 @@ func (h *ResponseHeader) SetContentTypeBytes(contentType []byte) {
 	h.contentType = append(h.contentType[:0], contentType...)
 }
 
+// ContentEncoding returns Content-Encoding header value.
+func (h *ResponseHeader) ContentEncoding() []byte {
+	return h.contentEncoding
+}
+
+// SetContentEncoding sets Content-Encoding header value.
+func (h *ResponseHeader) SetContentEncoding(contentEncoding string) {
+	h.contentEncoding = append(h.contentEncoding[:0], contentEncoding...)
+}
+
+// SetContentEncodingBytes sets Content-Encoding header value.
+func (h *ResponseHeader) SetContentEncodingBytes(contentEncoding []byte) {
+	h.contentEncoding = append(h.contentEncoding[:0], contentEncoding...)
+}
+
 // Server returns Server header value.
 func (h *ResponseHeader) Server() []byte {
 	return h.server
@@ -580,7 +596,7 @@ func (h *RequestHeader) SetReferer(referer string) {
 
 // SetRefererBytes sets Referer header value.
 func (h *RequestHeader) SetRefererBytes(referer []byte) {
-	h.SetCanonical(strReferer, referer)
+	h.setNonSpecial(strReferer, referer)
 }
 
 // Method returns HTTP request method.
@@ -937,6 +953,7 @@ func (h *ResponseHeader) resetSkipNormalize() {
 	h.contentLengthBytes = h.contentLengthBytes[:0]
 
 	h.contentType = h.contentType[:0]
+	h.contentEncoding = h.contentEncoding[:0]
 	h.server = h.server[:0]
 
 	h.h = h.h[:0]
@@ -994,6 +1011,7 @@ func (h *ResponseHeader) CopyTo(dst *ResponseHeader) {
 	dst.contentLength = h.contentLength
 	dst.contentLengthBytes = append(dst.contentLengthBytes, h.contentLengthBytes...)
 	dst.contentType = append(dst.contentType, h.contentType...)
+	dst.contentEncoding = append(dst.contentEncoding, h.contentEncoding...)
 	dst.server = append(dst.server, h.server...)
 	dst.h = copyArgs(dst.h, h.h)
 	dst.cookies = copyArgs(dst.cookies, h.cookies)
@@ -1035,6 +1053,10 @@ func (h *ResponseHeader) VisitAll(f func(key, value []byte)) {
 	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)
@@ -1158,6 +1180,8 @@ func (h *ResponseHeader) del(key []byte) {
 	switch string(key) {
 	case HeaderContentType:
 		h.contentType = h.contentType[:0]
+	case HeaderContentEncoding:
+		h.contentEncoding = h.contentEncoding[:0]
 	case HeaderServer:
 		h.server = h.server[:0]
 	case HeaderSetCookie:
@@ -1224,12 +1248,15 @@ func (h *ResponseHeader) setSpecialHeader(key, value []byte) bool {
 				h.contentLengthBytes = append(h.contentLengthBytes[:0], value...)
 			}
 			return true
+		} else if caseInsensitiveCompare(strContentEncoding, key) {
+			h.SetContentEncodingBytes(value)
+			return true
 		} else if caseInsensitiveCompare(strConnection, key) {
 			if bytes.Equal(strClose, value) {
 				h.SetConnectionClose()
 			} else {
 				h.ResetConnectionClose()
-				h.h = setArgBytes(h.h, key, value, argsHasValue)
+				h.setNonSpecial(key, value)
 			}
 			return true
 		}
@@ -1262,6 +1289,11 @@ func (h *ResponseHeader) setSpecialHeader(key, value []byte) bool {
 	return false
 }
 
+// setNonSpecial directly put into map i.e. not a basic header
+func (h *ResponseHeader) setNonSpecial(key []byte, value []byte) {
+	h.h = setArgBytes(h.h, key, value, argsHasValue)
+}
+
 // setSpecialHeader handles special headers and return true when a header is processed.
 func (h *RequestHeader) setSpecialHeader(key, value []byte) bool {
 	if len(key) == 0 {
@@ -1284,7 +1316,7 @@ func (h *RequestHeader) setSpecialHeader(key, value []byte) bool {
 				h.SetConnectionClose()
 			} else {
 				h.ResetConnectionClose()
-				h.h = setArgBytes(h.h, key, value, argsHasValue)
+				h.setNonSpecial(key, value)
 			}
 			return true
 		} else if caseInsensitiveCompare(strCookie, key) {
@@ -1315,6 +1347,11 @@ func (h *RequestHeader) setSpecialHeader(key, value []byte) bool {
 	return false
 }
 
+// setNonSpecial directly put into map i.e. not a basic header
+func (h *RequestHeader) setNonSpecial(key []byte, value []byte) {
+	h.h = setArgBytes(h.h, key, value, argsHasValue)
+}
+
 // Add adds the given 'key: value' header.
 //
 // Multiple headers with the same key may be added with this function.
@@ -1434,8 +1471,7 @@ func (h *ResponseHeader) SetCanonical(key, value []byte) {
 	if h.setSpecialHeader(key, value) {
 		return
 	}
-
-	h.h = setArgBytes(h.h, key, value, argsHasValue)
+	h.setNonSpecial(key, value)
 }
 
 // SetCookie sets the given response cookie.
@@ -1646,8 +1682,7 @@ func (h *RequestHeader) SetCanonical(key, value []byte) {
 	if h.setSpecialHeader(key, value) {
 		return
 	}
-
-	h.h = setArgBytes(h.h, key, value, argsHasValue)
+	h.setNonSpecial(key, value)
 }
 
 // Peek returns header value for the given key.
@@ -1696,6 +1731,8 @@ func (h *ResponseHeader) peek(key []byte) []byte {
 	switch string(key) {
 	case HeaderContentType:
 		return h.ContentType()
+	case HeaderContentEncoding:
+		return h.ContentEncoding()
 	case HeaderServer:
 		return h.Server()
 	case HeaderConnection:
@@ -2151,6 +2188,10 @@ func (h *ResponseHeader) AppendBytes(dst []byte) []byte {
 			dst = appendHeaderLine(dst, strContentType, contentType)
 		}
 	}
+	contentEncoding := h.ContentEncoding()
+	if len(contentEncoding) > 0 {
+		dst = appendHeaderLine(dst, strContentEncoding, contentEncoding)
+	}
 
 	if len(h.contentLengthBytes) > 0 {
 		dst = appendHeaderLine(dst, strContentLength, h.contentLengthBytes)
@@ -2616,6 +2657,10 @@ func (h *ResponseHeader) parseHeaders(buf []byte) (int, error) {
 					h.contentType = append(h.contentType[:0], s.value...)
 					continue
 				}
+				if caseInsensitiveCompare(s.key, strContentEncoding) {
+					h.contentEncoding = append(h.contentEncoding[:0], s.value...)
+					continue
+				}
 				if caseInsensitiveCompare(s.key, strContentLength) {
 					if h.contentLength != -1 {
 						if h.contentLength, err = parseContentLength(s.value); err != nil {
diff --git a/http.go b/http.go
index 384a7b3..9db9be5 100644
--- a/http.go
+++ b/http.go
@@ -1568,7 +1568,7 @@ func (resp *Response) WriteDeflateLevel(w *bufio.Writer, level int) error {
 }
 
 func (resp *Response) brotliBody(level int) error {
-	if len(resp.Header.peek(strContentEncoding)) > 0 {
+	if len(resp.Header.ContentEncoding()) > 0 {
 		// It looks like the body is already compressed.
 		// Do not compress it again.
 		return nil
@@ -1618,12 +1618,12 @@ func (resp *Response) brotliBody(level int) error {
 		resp.body = w
 		resp.bodyRaw = nil
 	}
-	resp.Header.SetCanonical(strContentEncoding, strBr)
+	resp.Header.SetContentEncodingBytes(strBr)
 	return nil
 }
 
 func (resp *Response) gzipBody(level int) error {
-	if len(resp.Header.peek(strContentEncoding)) > 0 {
+	if len(resp.Header.ContentEncoding()) > 0 {
 		// It looks like the body is already compressed.
 		// Do not compress it again.
 		return nil
@@ -1673,12 +1673,12 @@ func (resp *Response) gzipBody(level int) error {
 		resp.body = w
 		resp.bodyRaw = nil
 	}
-	resp.Header.SetCanonical(strContentEncoding, strGzip)
+	resp.Header.SetContentEncodingBytes(strGzip)
 	return nil
 }
 
 func (resp *Response) deflateBody(level int) error {
-	if len(resp.Header.peek(strContentEncoding)) > 0 {
+	if len(resp.Header.ContentEncoding()) > 0 {
 		// It looks like the body is already compressed.
 		// Do not compress it again.
 		return nil
@@ -1728,7 +1728,7 @@ func (resp *Response) deflateBody(level int) error {
 		resp.body = w
 		resp.bodyRaw = nil
 	}
-	resp.Header.SetCanonical(strContentEncoding, strDeflate)
+	resp.Header.SetContentEncodingBytes(strDeflate)
 	return nil
 }
 
diff --git a/server.go b/server.go
index e04c4ec..5e10887 100644
--- a/server.go
+++ b/server.go
@@ -1305,7 +1305,7 @@ func (ctx *RequestCtx) RedirectBytes(uri []byte, statusCode int) {
 }
 
 func (ctx *RequestCtx) redirect(uri []byte, statusCode int) {
-	ctx.Response.Header.SetCanonical(strLocation, uri)
+	ctx.Response.Header.setNonSpecial(strLocation, uri)
 	statusCode = getRedirectStatusCode(statusCode)
 	ctx.Response.SetStatusCode(statusCode)
 }
@@ -2376,7 +2376,7 @@ func (s *Server) serveConn(c net.Conn) (err error) {
 			// Set 'Connection: keep-alive' response header for HTTP/1.0 request.
 			// There is no need in setting this header for http/1.1, since in http/1.1
 			// connections are keep-alive by default.
-			ctx.Response.Header.SetCanonical(strConnection, strKeepAlive)
+			ctx.Response.Header.setNonSpecial(strConnection, strKeepAlive)
 		}
 
 		if serverName != nil && len(ctx.Response.Header.Server()) == 0 {


Test patch:
diff --git a/brotli_test.go b/brotli_test.go
index 15a76ac..0d7340a 100644
--- a/brotli_test.go
+++ b/brotli_test.go
@@ -120,7 +120,7 @@ func TestCompressHandlerBrotliLevel(t *testing.T) {
 	if err := resp.Read(br); err != nil {
 		t.Fatalf("unexpected error: %v", err)
 	}
-	ce := resp.Header.Peek(HeaderContentEncoding)
+	ce := resp.Header.ContentEncoding()
 	if string(ce) != "" {
 		t.Fatalf("unexpected Content-Encoding: %q. Expecting %q", ce, "")
 	}
@@ -140,7 +140,7 @@ func TestCompressHandlerBrotliLevel(t *testing.T) {
 	if err := resp.Read(br); err != nil {
 		t.Fatalf("unexpected error: %v", err)
 	}
-	ce = resp.Header.Peek(HeaderContentEncoding)
+	ce = resp.Header.ContentEncoding()
 	if string(ce) != "gzip" {
 		t.Fatalf("unexpected Content-Encoding: %q. Expecting %q", ce, "gzip")
 	}
@@ -163,7 +163,7 @@ func TestCompressHandlerBrotliLevel(t *testing.T) {
 	if err := resp.Read(br); err != nil {
 		t.Fatalf("unexpected error: %v", err)
 	}
-	ce = resp.Header.Peek(HeaderContentEncoding)
+	ce = resp.Header.ContentEncoding()
 	if string(ce) != "br" {
 		t.Fatalf("unexpected Content-Encoding: %q. Expecting %q", ce, "br")
 	}
diff --git a/fs_test.go b/fs_test.go
index de9f8c1..693d1a4 100644
--- a/fs_test.go
+++ b/fs_test.go
@@ -133,7 +133,7 @@ func TestServeFileHead(t *testing.T) {
 		t.Fatalf("unexpected error: %v", err)
 	}
 
-	ce := resp.Header.Peek(HeaderContentEncoding)
+	ce := resp.Header.ContentEncoding()
 	if len(ce) > 0 {
 		t.Fatalf("Unexpected 'Content-Encoding' %q", ce)
 	}
@@ -225,7 +225,7 @@ func TestServeFileCompressed(t *testing.T) {
 		t.Fatalf("unexpected error: %v", err)
 	}
 
-	ce := resp.Header.Peek(HeaderContentEncoding)
+	ce := resp.Header.ContentEncoding()
 	if string(ce) != "gzip" {
 		t.Fatalf("Unexpected 'Content-Encoding' %q. Expecting %q", ce, "gzip")
 	}
@@ -254,7 +254,7 @@ func TestServeFileCompressed(t *testing.T) {
 		t.Fatalf("unexpected error: %v", err)
 	}
 
-	ce = resp.Header.Peek(HeaderContentEncoding)
+	ce = resp.Header.ContentEncoding()
 	if string(ce) != "br" {
 		t.Fatalf("Unexpected 'Content-Encoding' %q. Expecting %q", ce, "br")
 	}
@@ -290,7 +290,7 @@ func TestServeFileUncompressed(t *testing.T) {
 		t.Fatalf("unexpected error: %v", err)
 	}
 
-	ce := resp.Header.Peek(HeaderContentEncoding)
+	ce := resp.Header.ContentEncoding()
 	if len(ce) > 0 {
 		t.Fatalf("Unexpected 'Content-Encoding' %q", ce)
 	}
@@ -567,7 +567,7 @@ func testFSCompress(t *testing.T, h RequestHandler, filePath string) {
 	if resp.StatusCode() != StatusOK {
 		t.Errorf("unexpected status code: %d. Expecting %d. filePath=%q", resp.StatusCode(), StatusOK, filePath)
 	}
-	ce := resp.Header.Peek(HeaderContentEncoding)
+	ce := resp.Header.ContentEncoding()
 	if string(ce) != "" {
 		t.Errorf("unexpected content-encoding %q. Expecting empty string. filePath=%q", ce, filePath)
 	}
@@ -586,7 +586,7 @@ func testFSCompress(t *testing.T, h RequestHandler, filePath string) {
 	if resp.StatusCode() != StatusOK {
 		t.Errorf("unexpected status code: %d. Expecting %d. filePath=%q", resp.StatusCode(), StatusOK, filePath)
 	}
-	ce = resp.Header.Peek(HeaderContentEncoding)
+	ce = resp.Header.ContentEncoding()
 	if string(ce) != "gzip" {
 		t.Errorf("unexpected content-encoding %q. Expecting %q. filePath=%q", ce, "gzip", filePath)
 	}
@@ -611,7 +611,7 @@ func testFSCompress(t *testing.T, h RequestHandler, filePath string) {
 	if resp.StatusCode() != StatusOK {
 		t.Errorf("unexpected status code: %d. Expecting %d. filePath=%q", resp.StatusCode(), StatusOK, filePath)
 	}
-	ce = resp.Header.Peek(HeaderContentEncoding)
+	ce = resp.Header.ContentEncoding()
 	if string(ce) != "br" {
 		t.Errorf("unexpected content-encoding %q. Expecting %q. filePath=%q", ce, "br", filePath)
 	}
diff --git a/header_test.go b/header_test.go
index c958dbd..ac8b5eb 100644
--- a/header_test.go
+++ b/header_test.go
@@ -33,6 +33,26 @@ func TestResponseHeaderAddContentType(t *testing.T) {
 	}
 }
 
+func TestResponseHeaderAddContentEncoding(t *testing.T) {
+	t.Parallel()
+
+	var h ResponseHeader
+	h.Add("Content-Encoding", "test")
+
+	got := string(h.Peek("Content-Encoding"))
+	expected := "test"
+	if got != expected {
+		t.Errorf("expected %q got %q", expected, got)
+	}
+
+	var buf bytes.Buffer
+	h.WriteTo(&buf) //nolint:errcheck
+
+	if n := strings.Count(buf.String(), "Content-Encoding: "); n != 1 {
+		t.Errorf("Content-Encoding occurred %d times", n)
+	}
+}
+
 func TestResponseHeaderMultiLineValue(t *testing.T) {
 	t.Parallel()
 
@@ -600,6 +620,7 @@ func TestResponseHeaderDel(t *testing.T) {
 	h.Set("aaa", "bbb")
 	h.Set(HeaderConnection, "keep-alive")
 	h.Set(HeaderContentType, "aaa")
+	h.Set(HeaderContentEncoding, "gzip")
 	h.Set(HeaderServer, "aaabbb")
 	h.Set(HeaderContentLength, "1123")
 	h.Set(HeaderTrailer, "foo, bar")
@@ -633,6 +654,10 @@ func TestResponseHeaderDel(t *testing.T) {
 	if string(hv) != string(defaultContentType) {
 		t.Fatalf("unexpected content-type: %q. Expecting %q", hv, defaultContentType)
 	}
+	hv = h.Peek(HeaderContentEncoding)
+	if string(hv) != ("gzip") {
+		t.Fatalf("unexpected content-encoding: %q. Expecting %q", hv, "gzip")
+	}
 	hv = h.Peek(HeaderServer)
 	if len(hv) > 0 {
 		t.Fatalf("non-zero value: %q", hv)
@@ -1274,6 +1299,7 @@ func TestResponseHeaderCopyTo(t *testing.T) {
 
 	h.Set(HeaderSetCookie, "foo=bar")
 	h.Set(HeaderContentType, "foobar")
+	h.Set(HeaderContentEncoding, "gzip")
 	h.Set("AAA-BBB", "aaaa")
 	h.Set(HeaderTrailer, "foo, bar")
 
@@ -1285,6 +1311,9 @@ func TestResponseHeaderCopyTo(t *testing.T) {
 	if !bytes.Equal(h1.Peek(HeaderContentType), h.Peek(HeaderContentType)) {
 		t.Fatalf("unexpected content-type %q. Expected %q", h1.Peek("content-type"), h.Peek("content-type"))
 	}
+	if !bytes.Equal(h1.Peek(HeaderContentEncoding), h.Peek(HeaderContentEncoding)) {
+		t.Fatalf("unexpected content-encoding %q. Expected %q", h1.Peek("content-encoding"), h.Peek("content-encoding"))
+	}
 	if !bytes.Equal(h1.Peek("aaa-bbb"), h.Peek("AAA-BBB")) {
 		t.Fatalf("unexpected aaa-bbb %q. Expected %q", h1.Peek("aaa-bbb"), h.Peek("aaa-bbb"))
 	}
@@ -1308,6 +1337,7 @@ func TestRequestHeaderCopyTo(t *testing.T) {
 
 	h.Set(HeaderCookie, "aa=bb; cc=dd")
 	h.Set(HeaderContentType, "foobar")
+	h.Set(HeaderContentEncoding, "gzip")
 	h.Set(HeaderHost, "aaaa")
 	h.Set("aaaxxx", "123")
 	h.Set(HeaderTrailer, "foo, bar")
@@ -1320,6 +1350,9 @@ func TestRequestHeaderCopyTo(t *testing.T) {
 	if !bytes.Equal(h1.Peek("content-type"), h.Peek(HeaderContentType)) {
 		t.Fatalf("unexpected content-type %q. Expected %q", h1.Peek("content-type"), h.Peek("content-type"))
 	}
+	if !bytes.Equal(h1.Peek("content-encoding"), h.Peek(HeaderContentEncoding)) {
+		t.Fatalf("unexpected content-encoding %q. Expected %q", h1.Peek("content-encoding"), h.Peek("content-encoding"))
+	}
 	if !bytes.Equal(h1.Peek("host"), h.Peek("host")) {
 		t.Fatalf("unexpected host %q. Expected %q", h1.Peek("host"), h.Peek("host"))
 	}
@@ -1513,17 +1546,18 @@ func TestResponseHeaderVisitAll(t *testing.T) {
 
 	var h ResponseHeader
 
-	r := bytes.NewBufferString("HTTP/1.1 200 OK\r\nContent-Type: aa\r\nContent-Length: 123\r\nSet-Cookie: aa=bb; path=/foo/bar\r\nSet-Cookie: ccc\r\nTrailer: Foo, Bar\r\n\r\n")
+	r := bytes.NewBufferString("HTTP/1.1 200 OK\r\nContent-Type: text/plain\r\nContent-Encoding: gzip\r\nContent-Length: 123\r\nSet-Cookie: aa=bb; path=/foo/bar\r\nSet-Cookie: ccc\r\nTrailer: Foo, Bar\r\n\r\n")
 	br := bufio.NewReader(r)
 	if err := h.Read(br); err != nil {
 		t.Fatalf("Unexpected error: %v", err)
 	}
 
-	if h.Len() != 5 {
-		t.Fatalf("Unexpected number of headers: %d. Expected 5", h.Len())
+	if h.Len() != 6 {
+		t.Fatalf("Unexpected number of headers: %d. Expected 6", h.Len())
 	}
 	contentLengthCount := 0
 	contentTypeCount := 0
+	contentEncodingCount := 0
 	cookieCount := 0
 	h.VisitAll(func(key, value []byte) {
 		k := string(key)
@@ -1539,6 +1573,11 @@ func TestResponseHeaderVisitAll(t *testing.T) {
 				t.Fatalf("Unexpected content-type: %q. Expected %q", v, h.Peek(k))
 			}
 			contentTypeCount++
+		case HeaderContentEncoding:
+			if v != string(h.Peek(k)) {
+				t.Fatalf("Unexpected content-encoding: %q. Expected %q", v, h.Peek(k))
+			}
+			contentEncodingCount++
 		case HeaderSetCookie:
 			if cookieCount == 0 && v != "aa=bb; path=/foo/bar" {
 				t.Fatalf("unexpected cookie header: %q. Expected %q", v, "aa=bb; path=/foo/bar")
@@ -1561,6 +1600,9 @@ func TestResponseHeaderVisitAll(t *testing.T) {
 	if contentTypeCount != 1 {
 		t.Fatalf("unexpected number of content-type headers: %d. Expected 1", contentTypeCount)
 	}
+	if contentEncodingCount != 1 {
+		t.Fatalf("unexpected number of content-encoding headers: %d. Expected 1", contentEncodingCount)
+	}
 	if cookieCount != 2 {
 		t.Fatalf("unexpected number of cookie header: %d. Expected 2", cookieCount)
 	}
@@ -2081,6 +2123,7 @@ func TestResponseHeaderSetGet(t *testing.T) {
 	h := &ResponseHeader{}
 	h.Set("foo", "bar")
 	h.Set("content-type", "aaa/bbb")
+	h.Set("content-encoding", "gzip")
 	h.Set("connection", "close")
 	h.Set("content-length", "1234")
 	h.Set(HeaderServer, "aaaa")
@@ -2089,6 +2132,7 @@ func TestResponseHeaderSetGet(t *testing.T) {
 
 	expectResponseHeaderGet(t, h, "Foo", "bar")
 	expectResponseHeaderGet(t, h, HeaderContentType, "aaa/bbb")
+	expectResponseHeaderGet(t, h, HeaderContentEncoding, "gzip")
 	expectResponseHeaderGet(t, h, HeaderConnection, "close")
 	expectResponseHeaderGet(t, h, HeaderContentLength, "1234")
 	expectResponseHeaderGet(t, h, "seRVer", "aaaa")
@@ -2127,6 +2171,7 @@ func TestResponseHeaderSetGet(t *testing.T) {
 
 	expectResponseHeaderGet(t, &h1, "Foo", "bar")
 	expectResponseHeaderGet(t, &h1, HeaderContentType, "aaa/bbb")
+	expectResponseHeaderGet(t, &h1, HeaderContentEncoding, "gzip")
 	expectResponseHeaderGet(t, &h1, HeaderConnection, "close")
 	expectResponseHeaderGet(t, &h1, "seRVer", "aaaa")
 	expectResponseHeaderGet(t, &h1, "baz", "xxxxx")
@@ -2244,14 +2289,14 @@ func TestResponseHeaderBufioPeek(t *testing.T) {
 	t.Parallel()
 
 	r := &bufioPeekReader{
-		s: "HTTP/1.1 200 OK\r\nContent-Length: 10\r\nContent-Type: aaa\r\n" + getHeaders(10) + "\r\n0123456789",
+		s: "HTTP/1.1 200 OK\r\nContent-Length: 10\r\nContent-Type: text/plain\r\nContent-Encoding: gzip\r\n" + getHeaders(10) + "\r\n0123456789",
 	}
 	br := bufio.NewReaderSize(r, 4096)
 	h := &ResponseHeader{}
 	if err := h.Read(br); err != nil {
 		t.Fatalf("Unexpected error when reading response: %v", err)
 	}
-	verifyResponseHeader(t, h, 200, 10, "aaa")
+	verifyResponseHeader(t, h, 200, 10, "text/plain", "gzip")
 }
 
 func getHeaders(n int) string {
@@ -2720,7 +2765,7 @@ func testResponseHeaderReadSuccess(t *testing.T, h *ResponseHeader, headers stri
 	if err != nil {
 		t.Fatalf("Unexpected error when parsing response headers: %v. headers=%q", err, headers)
 	}
-	verifyResponseHeader(t, h, expectedStatusCode, expectedContentLength, expectedContentType)
+	verifyResponseHeader(t, h, expectedStatusCode, expectedContentLength, expectedContentType, "")
 }
 
 func testRequestHeaderReadSuccess(t *testing.T, h *RequestHeader, headers string, expectedContentLength int,
@@ -2734,15 +2779,18 @@ func testRequestHeaderReadSuccess(t *testing.T, h *RequestHeader, headers string
 	verifyRequestHeader(t, h, expectedContentLength, expectedRequestURI, expectedHost, expectedReferer, expectedContentType)
 }
 
-func verifyResponseHeader(t *testing.T, h *ResponseHeader, expectedStatusCode, expectedContentLength int, expectedContentType string) {
+func verifyResponseHeader(t *testing.T, h *ResponseHeader, expectedStatusCode, expectedContentLength int, expectedContentType, expectedContentEncoding string) {
 	if h.StatusCode() != expectedStatusCode {
 		t.Fatalf("Unexpected status code %d. Expected %d", h.StatusCode(), expectedStatusCode)
 	}
 	if h.ContentLength() != expectedContentLength {
 		t.Fatalf("Unexpected content length %d. Expected %d", h.ContentLength(), expectedContentLength)
 	}
-	if string(h.Peek(HeaderContentType)) != expectedContentType {
-		t.Fatalf("Unexpected content type %q. Expected %q", h.Peek(HeaderContentType), expectedContentType)
+	if string(h.ContentType()) != expectedContentType {
+		t.Fatalf("Unexpected content type %q. Expected %q", h.ContentType(), expectedContentType)
+	}
+	if string(h.ContentEncoding()) != expectedContentEncoding {
+		t.Fatalf("Unexpected content encoding %q. Expected %q", h.ContentEncoding(), expectedContentEncoding)
 	}
 }
 
diff --git a/http_test.go b/http_test.go
index 71d5c6e..8d99cdd 100644
--- a/http_test.go
+++ b/http_test.go
@@ -1383,7 +1383,7 @@ func testResponseDeflateExt(t *testing.T, r *Response, s string) {
 		t.Fatalf("unexpected error: %v", err)
 	}
 
-	ce := r1.Header.Peek(HeaderContentEncoding)
+	ce := r1.Header.ContentEncoding()
 	var body []byte
 	if isCompressible {
 		if string(ce) != "deflate" {
@@ -1436,7 +1436,7 @@ func testResponseGzipExt(t *testing.T, r *Response, s string) {
 		t.Fatalf("unexpected error: %v", err)
 	}
 
-	ce := r1.Header.Peek(HeaderContentEncoding)
+	ce := r1.Header.ContentEncoding()
 	var body []byte
 	if isCompressible {
 		if string(ce) != "gzip" {
@@ -1992,7 +1992,7 @@ func testResponseReadWithoutBody(t *testing.T, resp *Response, s string, skipBod
 	if len(resp.Body()) != 0 {
 		t.Fatalf("Unexpected response body %q. Expected %q. response=%q", resp.Body(), "", s)
 	}
-	verifyResponseHeader(t, &resp.Header, expectedStatusCode, expectedContentLength, expectedContentType)
+	verifyResponseHeader(t, &resp.Header, expectedStatusCode, expectedContentLength, expectedContentType, "")
 	verifyResponseTrailer(t, &resp.Header, expectedTrailer)
 
 	// verify that ordinal response is read after null-body response
@@ -2269,7 +2269,7 @@ func testResponseReadSuccess(t *testing.T, resp *Response, response string, expe
 		t.Fatalf("Unexpected error: %v", err)
 	}
 
-	verifyResponseHeader(t, &resp.Header, expectedStatusCode, expectedContentLength, expectedContentType)
+	verifyResponseHeader(t, &resp.Header, expectedStatusCode, expectedContentLength, expectedContentType, "")
 	if !bytes.Equal(resp.Body(), []byte(expectedBody)) {
 		t.Fatalf("Unexpected body %q. Expected %q", resp.Body(), []byte(expectedBody))
 	}
diff --git a/server_test.go b/server_test.go
index 65bcce3..6775ce6 100644
--- a/server_test.go
+++ b/server_test.go
@@ -1943,7 +1943,7 @@ func TestCompressHandler(t *testing.T) {
 	if err := resp.Read(br); err != nil {
 		t.Fatalf("unexpected error: %v", err)
 	}
-	ce := resp.Header.Peek(HeaderContentEncoding)
+	ce := resp.Header.ContentEncoding()
 	if string(ce) != "" {
 		t.Fatalf("unexpected Content-Encoding: %q. Expecting %q", ce, "")
 	}
@@ -1963,7 +1963,7 @@ func TestCompressHandler(t *testing.T) {
 	if err := resp.Read(br); err != nil {
 		t.Fatalf("unexpected error: %v", err)
 	}
-	ce = resp.Header.Peek(HeaderContentEncoding)
+	ce = resp.Header.ContentEncoding()
 	if string(ce) != "gzip" {
 		t.Fatalf("unexpected Content-Encoding: %q. Expecting %q", ce, "gzip")
 	}
@@ -1986,7 +1986,7 @@ func TestCompressHandler(t *testing.T) {
 	if err := resp.Read(br); err != nil {
 		t.Fatalf("unexpected error: %v", err)
 	}
-	ce = resp.Header.Peek(HeaderContentEncoding)
+	ce = resp.Header.ContentEncoding()
 	if string(ce) != "gzip" {
 		t.Fatalf("unexpected Content-Encoding: %q. Expecting %q", ce, "gzip")
 	}
@@ -2009,7 +2009,7 @@ func TestCompressHandler(t *testing.T) {
 	if err := resp.Read(br); err != nil {
 		t.Fatalf("unexpected error: %v", err)
 	}
-	ce = resp.Header.Peek(HeaderContentEncoding)
+	ce = resp.Header.ContentEncoding()
 	if string(ce) != "deflate" {
 		t.Fatalf("unexpected Content-Encoding: %q. Expecting %q", ce, "deflate")
 	}
@@ -2985,7 +2985,7 @@ func TestServerMaxRequestsPerConn(t *testing.T) {
 	if !resp.ConnectionClose() {
 		t.Fatal("Response must have 'connection: close' header")
 	}
-	verifyResponseHeader(t, &resp.Header, 200, 0, string(defaultContentType))
+	verifyResponseHeader(t, &resp.Header, 200, 0, string(defaultContentType), "")
 
 	data, err := ioutil.ReadAll(br)
 	if err != nil {
@@ -4017,7 +4017,7 @@ func verifyResponse(t *testing.T, r *bufio.Reader, expectedStatusCode int, expec
 	if !bytes.Equal(resp.Body(), []byte(expectedBody)) {
 		t.Fatalf("Unexpected body %q. Expected %q", resp.Body(), []byte(expectedBody))
 	}
-	verifyResponseHeader(t, &resp.Header, expectedStatusCode, len(resp.Body()), expectedContentType)
+	verifyResponseHeader(t, &resp.Header, expectedStatusCode, len(resp.Body()), expectedContentType, "")
 	return &resp
 }