caddy/modules/caddyhttp/encode/sse_test.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)
}
}