135 lines
4.5 KiB
Go
135 lines
4.5 KiB
Go
// Copyright 2015 Matthew Holt and The Caddy Authors
|
|
//
|
|
// Licensed under the Apache License, Version 2.0 (the "License");
|
|
// you may not use this file except in compliance with the License.
|
|
// You may obtain a copy of the License at
|
|
//
|
|
// http://www.apache.org/licenses/LICENSE-2.0
|
|
//
|
|
// Unless required by applicable law or agreed to in writing, software
|
|
// distributed under the License is distributed on an "AS IS" BASIS,
|
|
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
// See the License for the specific language governing permissions and
|
|
// limitations under the License.
|
|
|
|
package encode_test
|
|
|
|
import (
|
|
"net/http"
|
|
"net/http/httptest"
|
|
"testing"
|
|
|
|
"github.com/caddyserver/caddy/v2"
|
|
"github.com/caddyserver/caddy/v2/caddyconfig"
|
|
"github.com/caddyserver/caddy/v2/modules/caddyhttp"
|
|
"github.com/caddyserver/caddy/v2/modules/caddyhttp/encode"
|
|
caddygzip "github.com/caddyserver/caddy/v2/modules/caddyhttp/encode/gzip"
|
|
)
|
|
|
|
// recordingWriter records the moment WriteHeader reaches the underlying
|
|
// writer, so a test can distinguish "headers flushed to the client" from
|
|
// "headers still buffered inside the encoder".
|
|
type recordingWriter struct {
|
|
http.ResponseWriter
|
|
wroteHeader bool
|
|
status int
|
|
}
|
|
|
|
func (rw *recordingWriter) WriteHeader(status int) {
|
|
if !rw.wroteHeader {
|
|
rw.wroteHeader = true
|
|
rw.status = status
|
|
}
|
|
rw.ResponseWriter.WriteHeader(status)
|
|
}
|
|
|
|
func (rw *recordingWriter) Flush() {
|
|
if f, ok := rw.ResponseWriter.(http.Flusher); ok {
|
|
f.Flush()
|
|
}
|
|
}
|
|
|
|
func newSSEEncodeHandler(t *testing.T) *encode.Encode {
|
|
t.Helper()
|
|
enc := &encode.Encode{
|
|
EncodingsRaw: caddy.ModuleMap{
|
|
"gzip": caddyconfig.JSON(caddygzip.Gzip{}, nil),
|
|
},
|
|
Prefer: []string{"gzip"},
|
|
// A large minimum_length means a normal small response would be
|
|
// buffered (its header withheld) until enough bytes arrive; the SSE
|
|
// path must bypass this so the handshake reaches the client.
|
|
MinLength: 4096,
|
|
}
|
|
ctx, cancel := caddy.NewContext(caddy.Context{Context: t.Context()})
|
|
t.Cleanup(cancel)
|
|
if err := enc.Provision(ctx); err != nil {
|
|
t.Fatalf("Provision() error = %v", err)
|
|
}
|
|
if err := enc.Validate(); err != nil {
|
|
t.Fatalf("Validate() error = %v", err)
|
|
}
|
|
return enc
|
|
}
|
|
|
|
// An SSE upstream typically writes headers and flushes to establish the
|
|
// event stream before any event body is available. The encode middleware
|
|
// must let those headers reach the client immediately rather than holding
|
|
// them for minimum_length content sniffing. See #6293.
|
|
func TestSSEHeadersFlushedBeforeBody(t *testing.T) {
|
|
enc := newSSEEncodeHandler(t)
|
|
r := httptest.NewRequest(http.MethodGet, "/", nil)
|
|
r.Header.Set("Accept-Encoding", "gzip")
|
|
baseRec := httptest.NewRecorder()
|
|
rec := &recordingWriter{ResponseWriter: baseRec}
|
|
|
|
next := caddyhttp.HandlerFunc(func(w http.ResponseWriter, _ *http.Request) error {
|
|
w.Header().Set("Content-Type", "text/event-stream")
|
|
w.WriteHeader(http.StatusOK)
|
|
// Flush the way a real SSE handler does — through the response
|
|
// controller (encode's writer implements FlushError, not Flush).
|
|
if err := http.NewResponseController(w).Flush(); err != nil {
|
|
t.Errorf("flush failed: %v", err)
|
|
}
|
|
// Before any event body is written, the client must already have the
|
|
// headers AND the flush must have reached the underlying writer.
|
|
if !rec.wroteHeader {
|
|
t.Error("SSE response headers were not written to the client before the body")
|
|
}
|
|
if rec.status != http.StatusOK {
|
|
t.Errorf("underlying status = %d, want 200", rec.status)
|
|
}
|
|
if !baseRec.Flushed {
|
|
t.Error("underlying ResponseRecorder was not flushed for the SSE handshake")
|
|
}
|
|
return nil
|
|
})
|
|
|
|
if err := enc.ServeHTTP(rec, r, next); err != nil {
|
|
t.Fatalf("ServeHTTP() error = %v", err)
|
|
}
|
|
}
|
|
|
|
// A normal (non-SSE) small response is still allowed to buffer its header
|
|
// for content sniffing — the SSE change must not force every response to
|
|
// flush its header early. This guards the scope of the fix.
|
|
func TestNonSSESmallResponseStillBuffersHeader(t *testing.T) {
|
|
enc := newSSEEncodeHandler(t)
|
|
r := httptest.NewRequest(http.MethodGet, "/", nil)
|
|
r.Header.Set("Accept-Encoding", "gzip")
|
|
rec := &recordingWriter{ResponseWriter: httptest.NewRecorder()}
|
|
|
|
next := caddyhttp.HandlerFunc(func(w http.ResponseWriter, _ *http.Request) error {
|
|
w.Header().Set("Content-Type", "text/plain")
|
|
w.WriteHeader(http.StatusOK)
|
|
if rec.wroteHeader {
|
|
t.Error("non-SSE response flushed its header early; SSE bypass leaked to normal responses")
|
|
}
|
|
return nil
|
|
})
|
|
|
|
if err := enc.ServeHTTP(rec, r, next); err != nil {
|
|
t.Fatalf("ServeHTTP() error = %v", err)
|
|
}
|
|
}
|