encode: Fix Accept-Ranges header; HEAD requests (#5039)

* fix encode handler header manipulation
also avoid implementing ReadFrom because it breaks when io.Copied to directly

* strconv.Itoa should be tried as a last resort
WriteHeader during Close
This commit is contained in:
WeidiDeng 2022-09-16 06:05:08 +08:00 committed by GitHub
parent f1f7a22674
commit 48d723c07c
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23

View file

@ -20,7 +20,6 @@
package encode package encode
import ( import (
"bytes"
"fmt" "fmt"
"io" "io"
"math" "math"
@ -160,13 +159,12 @@ func (enc *Encode) openResponseWriter(encodingName string, w http.ResponseWriter
// initResponseWriter initializes the responseWriter instance // initResponseWriter initializes the responseWriter instance
// allocated in openResponseWriter, enabling mid-stack inlining. // allocated in openResponseWriter, enabling mid-stack inlining.
func (enc *Encode) initResponseWriter(rw *responseWriter, encodingName string, wrappedRW http.ResponseWriter) *responseWriter { func (enc *Encode) initResponseWriter(rw *responseWriter, encodingName string, wrappedRW http.ResponseWriter) *responseWriter {
buf := bufPool.Get().(*bytes.Buffer) if httpInterfaces, ok := wrappedRW.(caddyhttp.HTTPInterfaces); ok {
buf.Reset() rw.HTTPInterfaces = httpInterfaces
} else {
// The allocation of ResponseWriterWrapper might be optimized as well. rw.HTTPInterfaces = &caddyhttp.ResponseWriterWrapper{ResponseWriter: wrappedRW}
rw.ResponseWriterWrapper = &caddyhttp.ResponseWriterWrapper{ResponseWriter: wrappedRW} }
rw.encodingName = encodingName rw.encodingName = encodingName
rw.buf = buf
rw.config = enc rw.config = enc
return rw return rw
@ -176,10 +174,9 @@ func (enc *Encode) initResponseWriter(rw *responseWriter, encodingName string, w
// using the encoding represented by encodingName and // using the encoding represented by encodingName and
// configured by config. // configured by config.
type responseWriter struct { type responseWriter struct {
*caddyhttp.ResponseWriterWrapper caddyhttp.HTTPInterfaces
encodingName string encodingName string
w Encoder w Encoder
buf *bytes.Buffer
config *Encode config *Encode
statusCode int statusCode int
wroteHeader bool wroteHeader bool
@ -206,28 +203,33 @@ func (rw *responseWriter) Flush() {
// to rw.Write (see bug in #4314) // to rw.Write (see bug in #4314)
return return
} }
rw.ResponseWriterWrapper.Flush() rw.HTTPInterfaces.Flush()
} }
// Write writes to the response. If the response qualifies, // Write writes to the response. If the response qualifies,
// it is encoded using the encoder, which is initialized // it is encoded using the encoder, which is initialized
// if not done so already. // if not done so already.
func (rw *responseWriter) Write(p []byte) (int, error) { func (rw *responseWriter) Write(p []byte) (int, error) {
var n, written int // ignore zero data writes, probably head request
var err error if len(p) == 0 {
return 0, nil
}
if rw.buf != nil && rw.config.MinLength > 0 { // sniff content-type and determine content-length
written = rw.buf.Len() if !rw.wroteHeader && rw.config.MinLength > 0 {
_, err := rw.buf.Write(p) var gtMinLength bool
if err != nil { if len(p) > rw.config.MinLength {
return 0, err gtMinLength = true
} else if cl, err := strconv.Atoi(rw.Header().Get("Content-Length")); err == nil && cl > rw.config.MinLength {
gtMinLength = true
}
if gtMinLength {
if rw.Header().Get("Content-Type") == "" {
rw.Header().Set("Content-Type", http.DetectContentType(p))
}
rw.init()
} }
rw.init()
p = rw.buf.Bytes()
defer func() {
bufPool.Put(rw.buf)
rw.buf = nil
}()
} }
// before we write to the response, we need to make // before we write to the response, we need to make
@ -236,63 +238,44 @@ func (rw *responseWriter) Write(p []byte) (int, error) {
// and if so, that means we haven't written the // and if so, that means we haven't written the
// header OR the default status code will be written // header OR the default status code will be written
// by the standard library // by the standard library
if rw.statusCode > 0 { if !rw.wroteHeader {
rw.ResponseWriter.WriteHeader(rw.statusCode) if rw.statusCode != 0 {
rw.statusCode = 0 rw.HTTPInterfaces.WriteHeader(rw.statusCode)
} else {
rw.HTTPInterfaces.WriteHeader(http.StatusOK)
}
rw.wroteHeader = true rw.wroteHeader = true
} }
switch { if rw.w != nil {
case rw.w != nil: return rw.w.Write(p)
n, err = rw.w.Write(p) } else {
default: return rw.HTTPInterfaces.Write(p)
n, err = rw.ResponseWriter.Write(p)
} }
n -= written
if n < 0 {
n = 0
}
return n, err
} }
// Close writes any remaining buffered response and // Close writes any remaining buffered response and
// deallocates any active resources. // deallocates any active resources.
func (rw *responseWriter) Close() error { func (rw *responseWriter) Close() error {
var err error // didn't write, probably head request
// only attempt to write the remaining buffered response if !rw.wroteHeader {
// if there are any bytes left to write; otherwise, if cl, err := strconv.Atoi(rw.Header().Get("Content-Length"))
// the handler above us returned an error without writing if err == nil && cl > rw.config.MinLength {
// anything, we'd write to the response when we instead rw.init()
// should simply let the error propagate back down; this }
// is why the check for rw.buf.Len() > 0 is crucial
if rw.buf != nil && rw.buf.Len() > 0 { if rw.statusCode != 0 {
rw.init() rw.HTTPInterfaces.WriteHeader(rw.statusCode)
p := rw.buf.Bytes() } else {
defer func() { rw.HTTPInterfaces.WriteHeader(http.StatusOK)
bufPool.Put(rw.buf)
rw.buf = nil
}()
switch {
case rw.w != nil:
_, err = rw.w.Write(p)
default:
_, err = rw.ResponseWriter.Write(p)
} }
} else if rw.statusCode != 0 {
// it is possible that a body was not written, and
// a header was not even written yet, even though
// we are closing; ensure the proper status code is
// written exactly once, or we risk breaking requests
// that rely on If-None-Match, for example
rw.ResponseWriter.WriteHeader(rw.statusCode)
rw.statusCode = 0
rw.wroteHeader = true rw.wroteHeader = true
} }
var err error
if rw.w != nil { if rw.w != nil {
err2 := rw.w.Close() err = rw.w.Close()
if err2 != nil && err == nil { rw.w.Reset(nil)
err = err2
}
rw.config.writerPools[rw.encodingName].Put(rw.w) rw.config.writerPools[rw.encodingName].Put(rw.w)
rw.w = nil rw.w = nil
} }
@ -302,16 +285,15 @@ func (rw *responseWriter) Close() error {
// init should be called before we write a response, if rw.buf has contents. // init should be called before we write a response, if rw.buf has contents.
func (rw *responseWriter) init() { func (rw *responseWriter) init() {
if rw.Header().Get("Content-Encoding") == "" && if rw.Header().Get("Content-Encoding") == "" &&
rw.buf.Len() >= rw.config.MinLength &&
rw.config.Match(rw) { rw.config.Match(rw) {
rw.w = rw.config.writerPools[rw.encodingName].Get().(Encoder) rw.w = rw.config.writerPools[rw.encodingName].Get().(Encoder)
rw.w.Reset(rw.ResponseWriter) rw.w.Reset(rw.HTTPInterfaces)
rw.Header().Del("Content-Length") // https://github.com/golang/go/issues/14975 rw.Header().Del("Content-Length") // https://github.com/golang/go/issues/14975
rw.Header().Set("Content-Encoding", rw.encodingName) rw.Header().Set("Content-Encoding", rw.encodingName)
rw.Header().Add("Vary", "Accept-Encoding") rw.Header().Add("Vary", "Accept-Encoding")
rw.Header().Del("Accept-Ranges") // we don't know ranges for dynamically-encoded content
} }
rw.Header().Del("Accept-Ranges") // we don't know ranges for dynamically-encoded content
} }
// AcceptedEncodings returns the list of encodings that the // AcceptedEncodings returns the list of encodings that the
@ -417,12 +399,6 @@ type Precompressed interface {
Suffix() string Suffix() string
} }
var bufPool = sync.Pool{
New: func() any {
return new(bytes.Buffer)
},
}
// defaultMinLength is the minimum length at which to compress content. // defaultMinLength is the minimum length at which to compress content.
const defaultMinLength = 512 const defaultMinLength = 512