1
+// Copyright 2015 Matthew Holt and The Caddy Authors
2
+//
3
+// Licensed under the Apache License, Version 2.0 (the "License");
4
+// you may not use this file except in compliance with the License.
5
+// You may obtain a copy of the License at
6
+//
7
+// http://www.apache.org/licenses/LICENSE-2.0
8
+//
9
+// Unless required by applicable law or agreed to in writing, software
10
+// distributed under the License is distributed on an "AS IS" BASIS,
11
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12
+// See the License for the specific language governing permissions and
13
+// limitations under the License.
14
+
15
+package encode_test
16
+
17
+import (
18
+ "net/http"
19
+ "net/http/httptest"
20
+ "testing"
21
+
22
+ "github.com/caddyserver/caddy/v2"
23
+ "github.com/caddyserver/caddy/v2/caddyconfig"
24
+ "github.com/caddyserver/caddy/v2/modules/caddyhttp"
25
+ "github.com/caddyserver/caddy/v2/modules/caddyhttp/encode"
26
+ caddygzip "github.com/caddyserver/caddy/v2/modules/caddyhttp/encode/gzip"
27
+)
28
+
29
+// recordingWriter records the moment WriteHeader reaches the underlying
30
+// writer, so a test can distinguish "headers flushed to the client" from
31
+// "headers still buffered inside the encoder".
32
+type recordingWriter struct {
33
+ http.ResponseWriter
34
+ wroteHeader bool
35
+ status int
36
+}
37
+
38
+func (rw *recordingWriter) WriteHeader(status int) {
39
+ if !rw.wroteHeader {
40
+ rw.wroteHeader = true
41
+ rw.status = status
42
+ }
43
+ rw.ResponseWriter.WriteHeader(status)
44
+}
45
+
46
+func (rw *recordingWriter) Flush() {
47
+ if f, ok := rw.ResponseWriter.(http.Flusher); ok {
48
+ f.Flush()
49
+ }
50
+}
51
+
52
+func newSSEEncodeHandler(t *testing.T) *encode.Encode {
53
+ t.Helper()
54
+ enc := &encode.Encode{
55
+ EncodingsRaw: caddy.ModuleMap{
56
+ "gzip": caddyconfig.JSON(caddygzip.Gzip{}, nil),
57
+ },
58
+ Prefer: []string{"gzip"},
59
+ // A large minimum_length means a normal small response would be
60
+ // buffered (its header withheld) until enough bytes arrive; the SSE
61
+ // path must bypass this so the handshake reaches the client.
62
+ MinLength: 4096,
63
+ }
64
+ ctx, cancel := caddy.NewContext(caddy.Context{Context: t.Context()})
65
+ t.Cleanup(cancel)
66
+ if err := enc.Provision(ctx); err != nil {
67
+ t.Fatalf("Provision() error = %v", err)
68
+ }
69
+ if err := enc.Validate(); err != nil {
70
+ t.Fatalf("Validate() error = %v", err)
71
+ }
72
+ return enc
73
+}
74
+
75
+// An SSE upstream typically writes headers and flushes to establish the
76
+// event stream before any event body is available. The encode middleware
77
+// must let those headers reach the client immediately rather than holding
78
+// them for minimum_length content sniffing. See #6293.
79
+func TestSSEHeadersFlushedBeforeBody(t *testing.T) {
80
+ enc := newSSEEncodeHandler(t)
81
+ r := httptest.NewRequest(http.MethodGet, "/", nil)
82
+ r.Header.Set("Accept-Encoding", "gzip")
83
+ baseRec := httptest.NewRecorder()
84
+ rec := &recordingWriter{ResponseWriter: baseRec}
85
+
86
+ next := caddyhttp.HandlerFunc(func(w http.ResponseWriter, _ *http.Request) error {
87
+ w.Header().Set("Content-Type", "text/event-stream")
88
+ w.WriteHeader(http.StatusOK)
89
+ // Flush the way a real SSE handler does — through the response
90
+ // controller (encode's writer implements FlushError, not Flush).
91
+ if err := http.NewResponseController(w).Flush(); err != nil {
92
+ t.Errorf("flush failed: %v", err)
93
+ }
94
+ // Before any event body is written, the client must already have the
95
+ // headers AND the flush must have reached the underlying writer.
96
+ if !rec.wroteHeader {
97
+ t.Error("SSE response headers were not written to the client before the body")
98
+ }
99
+ if rec.status != http.StatusOK {
100
+ t.Errorf("underlying status = %d, want 200", rec.status)
101
+ }
102
+ if !baseRec.Flushed {
103
+ t.Error("underlying ResponseRecorder was not flushed for the SSE handshake")
104
+ }
105
+ return nil
106
+ })
107
+
108
+ if err := enc.ServeHTTP(rec, r, next); err != nil {
109
+ t.Fatalf("ServeHTTP() error = %v", err)
110
+ }
111
+}
112
+
113
+// A normal (non-SSE) small response is still allowed to buffer its header
114
+// for content sniffing — the SSE change must not force every response to
115
+// flush its header early. This guards the scope of the fix.
116
+func TestNonSSESmallResponseStillBuffersHeader(t *testing.T) {
117
+ enc := newSSEEncodeHandler(t)
118
+ r := httptest.NewRequest(http.MethodGet, "/", nil)
119
+ r.Header.Set("Accept-Encoding", "gzip")
120
+ rec := &recordingWriter{ResponseWriter: httptest.NewRecorder()}
121
+
122
+ next := caddyhttp.HandlerFunc(func(w http.ResponseWriter, _ *http.Request) error {
123
+ w.Header().Set("Content-Type", "text/plain")
124
+ w.WriteHeader(http.StatusOK)
125
+ if rec.wroteHeader {
126
+ t.Error("non-SSE response flushed its header early; SSE bypass leaked to normal responses")
127
+ }
128
+ return nil
129
+ })
130
+
131
+ if err := enc.ServeHTTP(rec, r, next); err != nil {
132
+ t.Fatalf("ServeHTTP() error = %v", err)
133
+ }
134
+}