Fast and extensible multi-platform HTTP/1-2-3 web server with automatic HTTPS
Mirrored from https://github.com/Slazee/caddy.git
Synced Nov 11, 01:00 UTC
| 1 | package encode_test |
| 2 | |
| 3 | import ( |
| 4 | "bytes" |
| 5 | "fmt" |
| 6 | "io" |
| 7 | "net/http" |
| 8 | "net/http/httptest" |
| 9 | "slices" |
| 10 | "strings" |
| 11 | "testing" |
| 12 | "time" |
| 13 | |
| 14 | "golang.org/x/net/http/httpguts" |
| 15 | |
| 16 | "github.com/caddyserver/caddy/v2/modules/caddyhttp" |
| 17 | ) |
| 18 | |
| 19 | const conformanceContentType = "text/plain" |
| 20 | |
| 21 | func TestEncodeIfNoneMatch(t *testing.T) { |
| 22 | for _, encCase := range standardEncoderCases(t) { |
| 23 | t.Run(encCase.name, func(t *testing.T) { |
| 24 | enc := newEncodeHandler(t, encCase, 1) |
| 25 | for _, tc := range []struct { |
| 26 | name string |
| 27 | values []string |
| 28 | want []string |
| 29 | etag string |
| 30 | status int |
| 31 | }{ |
| 32 | {"single", []string{`"response-%s"`}, []string{`"response"`}, `"response"`, 304}, |
| 33 | {"matching first", []string{`"response-%s", "stale-%s"`}, []string{`"response", "stale"`}, `"response"`, 304}, |
| 34 | {"matching last", []string{`"stale-%s", "response-%s"`}, []string{`"stale", "response"`}, `"response"`, 304}, |
| 35 | {"weak", []string{`W/"response-%s"`}, []string{`W/"response"`}, `"response"`, 304}, |
| 36 | {"mixed", []string{`W/"stale-%s", "response-%s"`}, []string{`W/"stale", "response"`}, `"response"`, 304}, |
| 37 | {"quoted comma", []string{`"response,part-%s", "stale-%s"`}, []string{`"response,part", "stale"`}, `"response,part"`, 304}, |
| 38 | {"multiple lines", []string{`"stale-%s"`, `W/"response-%s"`}, []string{`"stale", W/"response"`}, `"response"`, 304}, |
| 39 | {"wildcard", []string{`*`}, []string{`*`}, `"response"`, 304}, |
| 40 | {"unencoded", []string{`"response"`}, []string{`"response"`}, `"response"`, 304}, |
| 41 | {"other suffix", []string{`"response-other"`}, []string{`"response-other"`}, `"response"`, 200}, |
| 42 | {"suffix inside tag", []string{`"response-%s-extra"`}, []string{`"response-%s-extra"`}, `"response"`, 200}, |
| 43 | {"unterminated", []string{`"response-%s`}, []string{`"response-%s`}, `"response"`, 200}, |
| 44 | } { |
| 45 | t.Run(tc.name, func(t *testing.T) { |
| 46 | r := httptest.NewRequest(http.MethodGet, "/", nil) |
| 47 | r.Header.Set("Accept-Encoding", encCase.encoding.AcceptEncoding()) |
| 48 | values, want := slices.Clone(tc.values), slices.Clone(tc.want) |
| 49 | for _, list := range [][]string{values, want} { |
| 50 | for i := range list { |
| 51 | list[i] = strings.ReplaceAll(list[i], "%s", encCase.encoding.AcceptEncoding()) |
| 52 | } |
| 53 | } |
| 54 | r.Header["If-None-Match"] = values |
| 55 | w := httptest.NewRecorder() |
| 56 | next := caddyhttp.HandlerFunc(func(w http.ResponseWriter, r *http.Request) error { |
| 57 | if got := r.Header.Values("If-None-Match"); !slices.Equal(got, want) { |
| 58 | t.Errorf("If-None-Match = %q, want %q", got, want) |
| 59 | } |
| 60 | w.Header().Set("Etag", tc.etag) |
| 61 | http.ServeContent(w, r, "asset.txt", time.Time{}, strings.NewReader("response body")) |
| 62 | return nil |
| 63 | }) |
| 64 | if err := enc.ServeHTTP(w, r, next); err != nil { |
| 65 | t.Fatal(err) |
| 66 | } |
| 67 | if w.Code != tc.status { |
| 68 | t.Errorf("status = %d, want %d", w.Code, tc.status) |
| 69 | } |
| 70 | }) |
| 71 | } |
| 72 | }) |
| 73 | } |
| 74 | } |
| 75 | |
| 76 | // TestStandardEncoderContract verifies Reset, Flush, Close, and Reset-after-Close |
| 77 | // reuse for each encoder using the same HTML/JSON/JS/CSS payloads as the benchmark suite. |
| 78 | func TestStandardEncoderContract(t *testing.T) { |
| 79 | for _, encCase := range standardEncoderCases(t) { |
| 80 | t.Run(encCase.name, func(t *testing.T) { |
| 81 | for _, corpus := range benchmarkCorpora(t) { |
| 82 | t.Run(corpus.name, func(t *testing.T) { |
| 83 | encoder := encCase.encoding.NewEncoder() |
| 84 | original := corpus.data |
| 85 | |
| 86 | encodeAndVerifyRoundTrip(t, encCase, encoder, original) |
| 87 | |
| 88 | // Simulate writer-pool reuse: Close → Reset(nil) → Reset(writer). |
| 89 | encoder.Reset(nil) |
| 90 | encodeAndVerifyRoundTrip(t, encCase, encoder, original) |
| 91 | }) |
| 92 | } |
| 93 | }) |
| 94 | } |
| 95 | } |
| 96 | |
| 97 | // TestEncodeCorpusResponse verifies encoded-response semantics (Content-Encoding, Vary, |
| 98 | // ETag suffix, header stripping) for each benchmark corpus and encoder. |
| 99 | func TestEncodeCorpusResponse(t *testing.T) { |
| 100 | for _, encCase := range standardEncoderCases(t) { |
| 101 | t.Run(encCase.name, func(t *testing.T) { |
| 102 | for _, corpus := range benchmarkCorpora(t) { |
| 103 | t.Run(corpus.name, func(t *testing.T) { |
| 104 | enc := newEncodeHandler(t, encCase, 1) |
| 105 | r := httptest.NewRequest(http.MethodGet, "/", nil) |
| 106 | r.Header.Set("Accept-Encoding", encCase.encoding.AcceptEncoding()) |
| 107 | w := httptest.NewRecorder() |
| 108 | |
| 109 | next := caddyhttp.HandlerFunc(func(w http.ResponseWriter, r *http.Request) error { |
| 110 | w.Header().Set("Content-Type", corpus.contentType) |
| 111 | w.Header().Set("Content-Length", fmt.Sprintf("%d", len(corpus.data))) |
| 112 | w.Header().Set("Accept-Ranges", "bytes") |
| 113 | w.Header().Set("Etag", `"response"`) |
| 114 | _, err := w.Write(corpus.data) |
| 115 | return err |
| 116 | }) |
| 117 | |
| 118 | if err := enc.ServeHTTP(w, r, next); err != nil { |
| 119 | t.Fatalf("ServeHTTP() error = %v", err) |
| 120 | } |
| 121 | checkEncodedCorpusResponse(t, w, encCase, corpus) |
| 122 | }) |
| 123 | } |
| 124 | }) |
| 125 | } |
| 126 | } |
| 127 | |
| 128 | type encodeScenario struct { |
| 129 | name string |
| 130 | method string |
| 131 | minLength int |
| 132 | reqHeaders func(encoderCase) http.Header |
| 133 | checkRequest func(*testing.T, *http.Request) |
| 134 | next func(encoderCase) caddyhttp.Handler |
| 135 | checkResponse func(*testing.T, *httptest.ResponseRecorder, encoderCase) |
| 136 | } |
| 137 | |
| 138 | var encodeScenarios = []encodeScenario{ |
| 139 | { |
| 140 | name: "minimum length prevents encoding", |
| 141 | method: http.MethodGet, |
| 142 | minLength: 1024, |
| 143 | next: func(encoderCase) caddyhttp.Handler { |
| 144 | return caddyhttp.HandlerFunc(func(w http.ResponseWriter, r *http.Request) error { |
| 145 | w.Header().Set("Content-Type", conformanceContentType) |
| 146 | _, err := w.Write([]byte("short")) |
| 147 | return err |
| 148 | }) |
| 149 | }, |
| 150 | checkResponse: checkMinLengthPreventsEncoding, |
| 151 | }, |
| 152 | { |
| 153 | name: "not modified adds vary without encoding", |
| 154 | method: http.MethodGet, |
| 155 | minLength: 1, |
| 156 | next: func(encoderCase) caddyhttp.Handler { |
| 157 | return caddyhttp.HandlerFunc(func(w http.ResponseWriter, r *http.Request) error { |
| 158 | w.WriteHeader(http.StatusNotModified) |
| 159 | return nil |
| 160 | }) |
| 161 | }, |
| 162 | checkResponse: checkNotModifiedVary, |
| 163 | }, |
| 164 | { |
| 165 | name: "head response headers can be encoded without body", |
| 166 | method: http.MethodHead, |
| 167 | minLength: 1, |
| 168 | next: func(encoderCase) caddyhttp.Handler { |
| 169 | return caddyhttp.HandlerFunc(func(w http.ResponseWriter, r *http.Request) error { |
| 170 | w.Header().Set("Content-Type", conformanceContentType) |
| 171 | w.Header().Set("Content-Length", "128") |
| 172 | return nil |
| 173 | }) |
| 174 | }, |
| 175 | checkResponse: checkHeadEncodedHeaders, |
| 176 | }, |
| 177 | { |
| 178 | name: "range response bypasses encoding", |
| 179 | method: http.MethodGet, |
| 180 | minLength: 1, |
| 181 | reqHeaders: func(encoderCase) http.Header { |
| 182 | return http.Header{"Range": {"bytes=0-15"}} |
| 183 | }, |
| 184 | next: func(encoderCase) caddyhttp.Handler { |
| 185 | return caddyhttp.HandlerFunc(func(w http.ResponseWriter, r *http.Request) error { |
| 186 | w.Header().Set("Content-Type", conformanceContentType) |
| 187 | w.Header().Set("Content-Range", "bytes 0-15/128") |
| 188 | w.Header().Set("Accept-Ranges", "bytes") |
| 189 | w.WriteHeader(http.StatusPartialContent) |
| 190 | _, err := w.Write([]byte("0123456789abcdef")) |
| 191 | return err |
| 192 | }) |
| 193 | }, |
| 194 | checkResponse: checkRangeResponseBypassesEncoding, |
| 195 | }, |
| 196 | { |
| 197 | name: "websocket handshake bypasses encoding", |
| 198 | method: http.MethodGet, |
| 199 | minLength: 1, |
| 200 | reqHeaders: func(encoderCase) http.Header { |
| 201 | return http.Header{ |
| 202 | "Connection": {"Upgrade"}, |
| 203 | "Sec-WebSocket-Key": {"dGhlIHNhbXBsZSBub25jZQ=="}, |
| 204 | "Upgrade": {"websocket"}, |
| 205 | } |
| 206 | }, |
| 207 | next: func(encoderCase) caddyhttp.Handler { |
| 208 | return caddyhttp.HandlerFunc(func(w http.ResponseWriter, r *http.Request) error { |
| 209 | w.WriteHeader(http.StatusSwitchingProtocols) |
| 210 | return nil |
| 211 | }) |
| 212 | }, |
| 213 | checkResponse: checkWebSocketBypass, |
| 214 | }, |
| 215 | { |
| 216 | name: "strips encoded etag suffix before next handler", |
| 217 | method: http.MethodGet, |
| 218 | minLength: 1, |
| 219 | reqHeaders: func(encCase encoderCase) http.Header { |
| 220 | return http.Header{ |
| 221 | "If-None-Match": {fmt.Sprintf(`"response-%s"`, encCase.encoding.AcceptEncoding())}, |
| 222 | } |
| 223 | }, |
| 224 | checkRequest: func(t *testing.T, r *http.Request) { |
| 225 | if got := r.Header.Get("If-None-Match"); got != `"response"` { |
| 226 | t.Fatalf("If-None-Match = %q, want %q", got, `"response"`) |
| 227 | } |
| 228 | }, |
| 229 | next: func(encoderCase) caddyhttp.Handler { |
| 230 | return caddyhttp.HandlerFunc(func(w http.ResponseWriter, r *http.Request) error { |
| 231 | w.WriteHeader(http.StatusNotModified) |
| 232 | return nil |
| 233 | }) |
| 234 | }, |
| 235 | checkResponse: checkStripsEncodedETagSuffix, |
| 236 | }, |
| 237 | { |
| 238 | name: "request cache-control no-transform does not prevent encoding", |
| 239 | method: http.MethodGet, |
| 240 | minLength: 1, |
| 241 | reqHeaders: func(encoderCase) http.Header { |
| 242 | return http.Header{"Cache-Control": {"no-cache, no-transform"}} |
| 243 | }, |
| 244 | next: func(encoderCase) caddyhttp.Handler { |
| 245 | return conformanceLargeBodyHandler(conformanceContentType) |
| 246 | }, |
| 247 | checkResponse: checkEncodesConformanceBody, |
| 248 | }, |
| 249 | { |
| 250 | name: "response cache-control no-transform prevents encoding", |
| 251 | method: http.MethodGet, |
| 252 | minLength: 1, |
| 253 | next: func(encoderCase) caddyhttp.Handler { |
| 254 | return caddyhttp.HandlerFunc(func(w http.ResponseWriter, r *http.Request) error { |
| 255 | w.Header().Set("Content-Type", conformanceContentType) |
| 256 | w.Header().Set("Cache-Control", "no-cache, no-transform") |
| 257 | _, err := w.Write(conformanceLargeBody()) |
| 258 | return err |
| 259 | }) |
| 260 | }, |
| 261 | checkResponse: checkBypassesEncoding, |
| 262 | }, |
| 263 | { |
| 264 | name: "response cache-control no-transform on a later line prevents encoding", |
| 265 | method: http.MethodGet, |
| 266 | minLength: 1, |
| 267 | next: func(encoderCase) caddyhttp.Handler { |
| 268 | return caddyhttp.HandlerFunc(func(w http.ResponseWriter, r *http.Request) error { |
| 269 | w.Header().Set("Content-Type", conformanceContentType) |
| 270 | w.Header()["Cache-Control"] = []string{"no-cache", "no-transform"} |
| 271 | _, err := w.Write(conformanceLargeBody()) |
| 272 | return err |
| 273 | }) |
| 274 | }, |
| 275 | checkResponse: checkBypassesEncoding, |
| 276 | }, |
| 277 | { |
| 278 | name: "content-encoding after an empty line prevents encoding", |
| 279 | method: http.MethodGet, |
| 280 | minLength: 1, |
| 281 | next: func(encoderCase) caddyhttp.Handler { |
| 282 | return caddyhttp.HandlerFunc(func(w http.ResponseWriter, r *http.Request) error { |
| 283 | w.Header().Set("Content-Type", conformanceContentType) |
| 284 | w.Header()["Content-Encoding"] = []string{"", "gzip"} |
| 285 | _, err := w.Write(conformanceLargeBody()) |
| 286 | return err |
| 287 | }) |
| 288 | }, |
| 289 | checkResponse: checkKeepsUpstreamContentEncoding, |
| 290 | }, |
| 291 | { |
| 292 | name: "content type matcher rejection prevents encoding", |
| 293 | method: http.MethodGet, |
| 294 | minLength: 1, |
| 295 | next: func(encoderCase) caddyhttp.Handler { |
| 296 | return conformanceLargeBodyHandler("image/png") |
| 297 | }, |
| 298 | checkResponse: checkBypassesEncoding, |
| 299 | }, |
| 300 | { |
| 301 | name: "missing accept-encoding keeps identity and varies", |
| 302 | method: http.MethodGet, |
| 303 | minLength: 1, |
| 304 | reqHeaders: func(encoderCase) http.Header { |
| 305 | return http.Header{"Accept-Encoding": nil} |
| 306 | }, |
| 307 | next: func(encoderCase) caddyhttp.Handler { |
| 308 | return conformanceLargeBodyHandler(conformanceContentType) |
| 309 | }, |
| 310 | checkResponse: checkIdentityVaries, |
| 311 | }, |
| 312 | { |
| 313 | name: "identity accept-encoding keeps identity and varies", |
| 314 | method: http.MethodGet, |
| 315 | minLength: 1, |
| 316 | reqHeaders: func(encoderCase) http.Header { |
| 317 | return http.Header{"Accept-Encoding": {"identity"}} |
| 318 | }, |
| 319 | next: func(encoderCase) caddyhttp.Handler { |
| 320 | return conformanceLargeBodyHandler(conformanceContentType) |
| 321 | }, |
| 322 | checkResponse: checkIdentityVaries, |
| 323 | }, |
| 324 | { |
| 325 | name: "missing accept-encoding with content type matcher rejection does not vary", |
| 326 | method: http.MethodGet, |
| 327 | minLength: 1, |
| 328 | reqHeaders: func(encoderCase) http.Header { |
| 329 | return http.Header{"Accept-Encoding": nil} |
| 330 | }, |
| 331 | next: func(encoderCase) caddyhttp.Handler { |
| 332 | return conformanceLargeBodyHandler("image/png") |
| 333 | }, |
| 334 | checkResponse: checkBypassesEncoding, |
| 335 | }, |
| 336 | } |
| 337 | |
| 338 | // TestEncodeResponseSemantics verifies HTTP edge cases (304, HEAD, range, WebSocket, |
| 339 | // minimum_length, ETag request rewriting, no-transform, matcher rejection) independent |
| 340 | // of the benchmark corpora. |
| 341 | func TestEncodeResponseSemantics(t *testing.T) { |
| 342 | for _, encCase := range standardEncoderCases(t) { |
| 343 | t.Run(encCase.name, func(t *testing.T) { |
| 344 | for _, sc := range encodeScenarios { |
| 345 | t.Run(sc.name, func(t *testing.T) { |
| 346 | runEncodeScenario(t, encCase, sc) |
| 347 | }) |
| 348 | } |
| 349 | }) |
| 350 | } |
| 351 | } |
| 352 | |
| 353 | func runEncodeScenario(t *testing.T, encCase encoderCase, sc encodeScenario) { |
| 354 | t.Helper() |
| 355 | |
| 356 | enc := newEncodeHandler(t, encCase, sc.minLength) |
| 357 | r := httptest.NewRequest(sc.method, "/", nil) |
| 358 | r.Header.Set("Accept-Encoding", encCase.encoding.AcceptEncoding()) |
| 359 | if sc.reqHeaders != nil { |
| 360 | for name, values := range sc.reqHeaders(encCase) { |
| 361 | r.Header.Del(name) |
| 362 | for _, value := range values { |
| 363 | r.Header.Add(name, value) |
| 364 | } |
| 365 | } |
| 366 | } |
| 367 | w := httptest.NewRecorder() |
| 368 | |
| 369 | var rw http.ResponseWriter = w |
| 370 | if sc.method == http.MethodHead { |
| 371 | // httptest.ResponseRecorder still stores body writes on HEAD; discard them |
| 372 | // so Close() path matches real clients that must not receive a body. |
| 373 | rw = noBodyResponseWriter{ResponseRecorder: w} |
| 374 | } |
| 375 | |
| 376 | next := sc.next(encCase) |
| 377 | if sc.checkRequest != nil { |
| 378 | inner := next |
| 379 | next = caddyhttp.HandlerFunc(func(w http.ResponseWriter, r *http.Request) error { |
| 380 | sc.checkRequest(t, r) |
| 381 | return inner.ServeHTTP(w, r) |
| 382 | }) |
| 383 | } |
| 384 | |
| 385 | if err := enc.ServeHTTP(rw, r, next); err != nil { |
| 386 | t.Fatalf("%s: ServeHTTP() error = %v", sc.name, err) |
| 387 | } |
| 388 | sc.checkResponse(t, w, encCase) |
| 389 | } |
| 390 | |
| 391 | // noBodyResponseWriter discards Write data while still allowing the encode |
| 392 | // middleware to observe writes for Content-Length / min-length decisions. |
| 393 | type noBodyResponseWriter struct { |
| 394 | *httptest.ResponseRecorder |
| 395 | } |
| 396 | |
| 397 | func (w noBodyResponseWriter) Write(p []byte) (int, error) { |
| 398 | return len(p), nil |
| 399 | } |
| 400 | |
| 401 | func checkEncodedCorpusResponse(t *testing.T, w *httptest.ResponseRecorder, encCase encoderCase, corpus benchmarkCorpus) { |
| 402 | t.Helper() |
| 403 | |
| 404 | encName := encCase.encoding.AcceptEncoding() |
| 405 | if got := w.Header().Get("Content-Encoding"); got != encName { |
| 406 | t.Fatalf("Content-Encoding = %q, want %q", got, encName) |
| 407 | } |
| 408 | if !httpguts.HeaderValuesContainsToken(w.Header()["Vary"], "Accept-Encoding") { |
| 409 | t.Fatalf("Vary = %q, want Accept-Encoding", w.Header().Values("Vary")) |
| 410 | } |
| 411 | if got := w.Header().Get("Content-Length"); got != "" { |
| 412 | t.Fatalf("Content-Length = %q, want empty", got) |
| 413 | } |
| 414 | if got := w.Header().Get("Accept-Ranges"); got != "" { |
| 415 | t.Fatalf("Accept-Ranges = %q, want empty", got) |
| 416 | } |
| 417 | wantETag := fmt.Sprintf(`"response-%s"`, encName) |
| 418 | if got := w.Header().Get("Etag"); got != wantETag { |
| 419 | t.Fatalf("Etag = %q, want %q", got, wantETag) |
| 420 | } |
| 421 | assertDecompresses(t, encCase, w.Body.Bytes(), corpus.data) |
| 422 | } |
| 423 | |
| 424 | func checkMinLengthPreventsEncoding(t *testing.T, w *httptest.ResponseRecorder, encCase encoderCase) { |
| 425 | t.Helper() |
| 426 | |
| 427 | if got := w.Header().Get("Content-Encoding"); got != "" { |
| 428 | t.Fatalf("Content-Encoding = %q, want empty", got) |
| 429 | } |
| 430 | if got := w.Body.String(); got != "short" { |
| 431 | t.Fatalf("body = %q, want short", got) |
| 432 | } |
| 433 | } |
| 434 | |
| 435 | func checkNotModifiedVary(t *testing.T, w *httptest.ResponseRecorder, encCase encoderCase) { |
| 436 | t.Helper() |
| 437 | |
| 438 | if got := w.Code; got != http.StatusNotModified { |
| 439 | t.Fatalf("status = %d, want %d", got, http.StatusNotModified) |
| 440 | } |
| 441 | if got := w.Header().Get("Content-Encoding"); got != "" { |
| 442 | t.Fatalf("Content-Encoding = %q, want empty", got) |
| 443 | } |
| 444 | if !httpguts.HeaderValuesContainsToken(w.Header()["Vary"], "Accept-Encoding") { |
| 445 | t.Fatalf("Vary = %q, want Accept-Encoding", w.Header().Values("Vary")) |
| 446 | } |
| 447 | if got := w.Body.Len(); got != 0 { |
| 448 | t.Fatalf("body length = %d, want 0", got) |
| 449 | } |
| 450 | } |
| 451 | |
| 452 | func checkHeadEncodedHeaders(t *testing.T, w *httptest.ResponseRecorder, encCase encoderCase) { |
| 453 | t.Helper() |
| 454 | |
| 455 | if got := w.Header().Get("Content-Encoding"); got != encCase.encoding.AcceptEncoding() { |
| 456 | t.Fatalf("Content-Encoding = %q, want %q", got, encCase.encoding.AcceptEncoding()) |
| 457 | } |
| 458 | if got := w.Body.Len(); got != 0 { |
| 459 | t.Fatalf("body length = %d, want 0", got) |
| 460 | } |
| 461 | } |
| 462 | |
| 463 | func checkRangeResponseBypassesEncoding(t *testing.T, w *httptest.ResponseRecorder, encCase encoderCase) { |
| 464 | t.Helper() |
| 465 | |
| 466 | if got := w.Code; got != http.StatusPartialContent { |
| 467 | t.Fatalf("status = %d, want %d", got, http.StatusPartialContent) |
| 468 | } |
| 469 | if got := w.Header().Get("Content-Encoding"); got != "" { |
| 470 | t.Fatalf("Content-Encoding = %q, want empty", got) |
| 471 | } |
| 472 | if got := w.Header().Get("Content-Range"); got != "bytes 0-15/128" { |
| 473 | t.Fatalf("Content-Range = %q, want %q", got, "bytes 0-15/128") |
| 474 | } |
| 475 | if got := w.Header().Get("Accept-Ranges"); got != "bytes" { |
| 476 | t.Fatalf("Accept-Ranges = %q, want bytes", got) |
| 477 | } |
| 478 | if got := w.Body.String(); got != "0123456789abcdef" { |
| 479 | t.Fatalf("body = %q, want %q", got, "0123456789abcdef") |
| 480 | } |
| 481 | } |
| 482 | |
| 483 | func checkWebSocketBypass(t *testing.T, w *httptest.ResponseRecorder, encCase encoderCase) { |
| 484 | t.Helper() |
| 485 | |
| 486 | if got := w.Code; got != http.StatusSwitchingProtocols { |
| 487 | t.Fatalf("status = %d, want %d", got, http.StatusSwitchingProtocols) |
| 488 | } |
| 489 | if got := w.Header().Get("Content-Encoding"); got != "" { |
| 490 | t.Fatalf("Content-Encoding = %q, want empty", got) |
| 491 | } |
| 492 | } |
| 493 | |
| 494 | func checkStripsEncodedETagSuffix(t *testing.T, w *httptest.ResponseRecorder, encCase encoderCase) { |
| 495 | t.Helper() |
| 496 | |
| 497 | if got := w.Code; got != http.StatusNotModified { |
| 498 | t.Fatalf("status = %d, want %d", got, http.StatusNotModified) |
| 499 | } |
| 500 | if !httpguts.HeaderValuesContainsToken(w.Header()["Vary"], "Accept-Encoding") { |
| 501 | t.Fatalf("Vary = %q, want Accept-Encoding", w.Header().Values("Vary")) |
| 502 | } |
| 503 | } |
| 504 | |
| 505 | func conformanceLargeBodyHandler(contentType string) caddyhttp.Handler { |
| 506 | body := conformanceLargeBody() |
| 507 | return caddyhttp.HandlerFunc(func(w http.ResponseWriter, r *http.Request) error { |
| 508 | w.Header().Set("Content-Type", contentType) |
| 509 | _, err := w.Write(body) |
| 510 | return err |
| 511 | }) |
| 512 | } |
| 513 | |
| 514 | func checkEncodesConformanceBody(t *testing.T, w *httptest.ResponseRecorder, encCase encoderCase) { |
| 515 | t.Helper() |
| 516 | |
| 517 | encName := encCase.encoding.AcceptEncoding() |
| 518 | if got := w.Header().Get("Content-Encoding"); got != encName { |
| 519 | t.Fatalf("Content-Encoding = %q, want %q", got, encName) |
| 520 | } |
| 521 | assertDecompresses(t, encCase, w.Body.Bytes(), conformanceLargeBody()) |
| 522 | } |
| 523 | |
| 524 | func checkBypassesEncoding(t *testing.T, w *httptest.ResponseRecorder, encCase encoderCase) { |
| 525 | t.Helper() |
| 526 | |
| 527 | want := conformanceLargeBody() |
| 528 | if got := w.Header().Get("Content-Encoding"); got != "" { |
| 529 | t.Fatalf("Content-Encoding = %q, want empty", got) |
| 530 | } |
| 531 | if !bytes.Equal(w.Body.Bytes(), want) { |
| 532 | t.Fatalf("body len = %d, want len = %d", w.Body.Len(), len(want)) |
| 533 | } |
| 534 | if httpguts.HeaderValuesContainsToken(w.Header()["Vary"], "Accept-Encoding") { |
| 535 | t.Fatalf("Vary = %q, want no Accept-Encoding", w.Header().Values("Vary")) |
| 536 | } |
| 537 | } |
| 538 | |
| 539 | func checkIdentityVaries(t *testing.T, w *httptest.ResponseRecorder, encCase encoderCase) { |
| 540 | t.Helper() |
| 541 | |
| 542 | want := conformanceLargeBody() |
| 543 | if got := w.Header().Get("Content-Encoding"); got != "" { |
| 544 | t.Fatalf("Content-Encoding = %q, want empty", got) |
| 545 | } |
| 546 | if !bytes.Equal(w.Body.Bytes(), want) { |
| 547 | t.Fatalf("body len = %d, want len = %d", w.Body.Len(), len(want)) |
| 548 | } |
| 549 | if !httpguts.HeaderValuesContainsToken(w.Header()["Vary"], "Accept-Encoding") { |
| 550 | t.Fatalf("Vary = %q, want Accept-Encoding", w.Header().Values("Vary")) |
| 551 | } |
| 552 | } |
| 553 | |
| 554 | func checkKeepsUpstreamContentEncoding(t *testing.T, w *httptest.ResponseRecorder, encCase encoderCase) { |
| 555 | t.Helper() |
| 556 | |
| 557 | if got, want := w.Header().Values("Content-Encoding"), []string{"", "gzip"}; !slices.Equal(got, want) { |
| 558 | t.Fatalf("Content-Encoding = %q, want %q", got, want) |
| 559 | } |
| 560 | if want := conformanceLargeBody(); !bytes.Equal(w.Body.Bytes(), want) { |
| 561 | t.Fatalf("body len = %d, want len = %d", w.Body.Len(), len(want)) |
| 562 | } |
| 563 | } |
| 564 | |
| 565 | // TestEncodeHeadResponseContentLength verifies that a HEAD response does not |
| 566 | // advertise the length of an empty encoded stream. |
| 567 | func TestEncodeHeadResponseContentLength(t *testing.T) { |
| 568 | for _, encCase := range standardEncoderCases(t) { |
| 569 | t.Run(encCase.name, func(t *testing.T) { |
| 570 | enc := newEncodeHandler(t, encCase, 1) |
| 571 | next := caddyhttp.HandlerFunc(func(w http.ResponseWriter, r *http.Request) error { |
| 572 | w.Header().Set("Content-Type", conformanceContentType) |
| 573 | w.Header().Set("Content-Length", "128") |
| 574 | return nil |
| 575 | }) |
| 576 | srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { |
| 577 | if err := enc.ServeHTTP(w, r, next); err != nil { |
| 578 | t.Errorf("ServeHTTP() error = %v", err) |
| 579 | } |
| 580 | })) |
| 581 | defer srv.Close() |
| 582 | |
| 583 | req, err := http.NewRequest(http.MethodHead, srv.URL, nil) |
| 584 | if err != nil { |
| 585 | t.Fatal(err) |
| 586 | } |
| 587 | req.Header.Set("Accept-Encoding", encCase.encoding.AcceptEncoding()) |
| 588 | resp, err := srv.Client().Do(req) |
| 589 | if err != nil { |
| 590 | t.Fatal(err) |
| 591 | } |
| 592 | resp.Body.Close() |
| 593 | |
| 594 | if got := resp.Header.Get("Content-Encoding"); got != encCase.encoding.AcceptEncoding() { |
| 595 | t.Fatalf("Content-Encoding = %q, want %q", got, encCase.encoding.AcceptEncoding()) |
| 596 | } |
| 597 | if resp.ContentLength != -1 { |
| 598 | t.Fatalf("ContentLength = %d, want -1", resp.ContentLength) |
| 599 | } |
| 600 | }) |
| 601 | } |
| 602 | } |
| 603 | |
| 604 | // TestEncodeHeaderOnlyResponseHasNoBody verifies that a response whose header |
| 605 | // is written without any body does not get an empty encoded stream as its body. |
| 606 | func TestEncodeHeaderOnlyResponseHasNoBody(t *testing.T) { |
| 607 | for _, encCase := range standardEncoderCases(t) { |
| 608 | t.Run(encCase.name, func(t *testing.T) { |
| 609 | enc := newEncodeHandler(t, encCase, 1) |
| 610 | next := caddyhttp.HandlerFunc(func(w http.ResponseWriter, r *http.Request) error { |
| 611 | w.Header().Set("Content-Type", "text/event-stream") |
| 612 | w.WriteHeader(http.StatusOK) |
| 613 | return nil |
| 614 | }) |
| 615 | srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { |
| 616 | if err := enc.ServeHTTP(w, r, next); err != nil { |
| 617 | t.Errorf("ServeHTTP() error = %v", err) |
| 618 | } |
| 619 | })) |
| 620 | defer srv.Close() |
| 621 | |
| 622 | req, err := http.NewRequest(http.MethodGet, srv.URL, nil) |
| 623 | if err != nil { |
| 624 | t.Fatal(err) |
| 625 | } |
| 626 | req.Header.Set("Accept-Encoding", encCase.encoding.AcceptEncoding()) |
| 627 | resp, err := srv.Client().Do(req) |
| 628 | if err != nil { |
| 629 | t.Fatal(err) |
| 630 | } |
| 631 | body, err := io.ReadAll(resp.Body) |
| 632 | resp.Body.Close() |
| 633 | if err != nil { |
| 634 | t.Fatal(err) |
| 635 | } |
| 636 | |
| 637 | if got := resp.Header.Get("Content-Encoding"); got != encCase.encoding.AcceptEncoding() { |
| 638 | t.Fatalf("Content-Encoding = %q, want %q", got, encCase.encoding.AcceptEncoding()) |
| 639 | } |
| 640 | if len(body) != 0 { |
| 641 | t.Fatalf("body len = %d, want 0", len(body)) |
| 642 | } |
| 643 | }) |
| 644 | } |
| 645 | } |