| index 651fd1df7..59008f1ef 100644 |
| --- a/internal/edge/edge_test.go |
| +++ b/internal/edge/edge_test.go |
| @@ -13,6 +13,7 @@ import ( |
| "net/http/httptest" |
| "os" |
| "path/filepath" |
| + "slices" |
| "strings" |
| "sync/atomic" |
| "syscall" |
| @@ -1412,3 +1413,54 @@ func TestCacheBeforeWakeFullCycle(t *testing.T) { |
| t.Fatalf("expected the post-purge request to miss and wake once more, wakes = %d", wakes) |
| } |
| } |
| + |
| +type mockResponseWriterFor1xx struct { |
| + header http.Header |
| + codes []int |
| +} |
| + |
| +func (m *mockResponseWriterFor1xx) Header() http.Header { |
| + return m.header |
| +} |
| + |
| +func (m *mockResponseWriterFor1xx) WriteHeader(status int) { |
| + // Stand-in for a real ResponseWriter. httptest.ResponseRecorder rejects |
| + // writes after a 1xx status (RFC 9110 forbids bodies on 1xx), but a real |
| + // ResponseWriter accepts the sequence 1xx then 2xx (103 Early Hints |
| + // followed by the final response status) and forwards both. This mock |
| + // accepts and records both. |
| + m.codes = append(m.codes, status) |
| +} |
| + |
| +func (m *mockResponseWriterFor1xx) Write(p []byte) (int, error) { |
| + return len(p), nil |
| +} |
| + |
| +func TestRecordingWriterIgnoresInformational(t *testing.T) { |
| + mock := &mockResponseWriterFor1xx{header: make(http.Header)} |
| + |
| + rec := &recordingResponseWriter{ |
| + ResponseWriter: mock, |
| + request: httptest.NewRequest("GET", "http://app.example/x", nil), |
| + cacheable: true, |
| + maxBody: 1 << 20, |
| + } |
| + rec.WriteHeader(http.StatusEarlyHints) |
| + rec.Header().Set("Cache-Control", "max-age=60") |
| + rec.WriteHeader(http.StatusOK) |
| + if _, err := rec.Write([]byte("final body")); err != nil { |
| + t.Fatal(err) |
| + } |
| + if rec.status != http.StatusOK { |
| + t.Fatalf("recorded status %d, want %d — informational must not latch", rec.status, http.StatusOK) |
| + } |
| + if !slices.Equal(mock.codes, []int{http.StatusEarlyHints, http.StatusOK}) { |
| + t.Fatalf("underlying writer saw %v, want [103 200] — the 103 must be forwarded, not swallowed", mock.codes) |
| + } |
| + e := &Edge{cache: NewCache(DefaultCacheMaxEntries, DefaultCacheMaxBodyBytes)} |
| + key := CacheKey{Host: "app.example", Method: "GET", Path: "/x"} |
| + e.maybeStore(key, rec) |
| + if _, ok := e.cache.get(key); !ok { |
| + t.Fatal("response written after a 103 was not cached") |
| + } |
| +} |