|
4 | 4 | "context" |
5 | 5 | "errors" |
6 | 6 | "net/http" |
| 7 | + "slices" |
7 | 8 | "testing" |
8 | 9 | "time" |
9 | 10 |
|
@@ -57,6 +58,35 @@ func (w *WrappedDeadliner) SetWriteDeadline(t time.Time) error { |
57 | 58 | return w.deadlineErr |
58 | 59 | } |
59 | 60 |
|
| 61 | +// orderedWriter records the order in which WriteHeader, Write, and Flush |
| 62 | +// are called so tests can assert headers are committed before body writes. |
| 63 | +type orderedWriter struct { |
| 64 | + events []string |
| 65 | + status int |
| 66 | +} |
| 67 | + |
| 68 | +func (w *orderedWriter) Header() http.Header { |
| 69 | + return http.Header{} |
| 70 | +} |
| 71 | + |
| 72 | +func (w *orderedWriter) Write(p []byte) (int, error) { |
| 73 | + w.events = append(w.events, "write") |
| 74 | + return len(p), nil |
| 75 | +} |
| 76 | + |
| 77 | +func (w *orderedWriter) WriteHeader(statusCode int) { |
| 78 | + w.status = statusCode |
| 79 | + w.events = append(w.events, "writeHeader") |
| 80 | +} |
| 81 | + |
| 82 | +func (w *orderedWriter) Flush() { |
| 83 | + w.events = append(w.events, "flush") |
| 84 | +} |
| 85 | + |
| 86 | +func (w *orderedWriter) SetWriteDeadline(t time.Time) error { |
| 87 | + return nil |
| 88 | +} |
| 89 | + |
60 | 90 | type sseTest struct { |
61 | 91 | Title string |
62 | 92 | TestFunc func(t *testing.T) |
@@ -131,6 +161,50 @@ data: {"error": "encode error: json: unsupported type: chan int"} |
131 | 161 | api.Adapter().ServeHTTP(w, req) |
132 | 162 | }, |
133 | 163 | }, |
| 164 | + { |
| 165 | + Title: "sse flushes headers before first event", |
| 166 | + TestFunc: func(t *testing.T) { |
| 167 | + _, api := humatest.New(t) |
| 168 | + started := make(chan struct{}) |
| 169 | + release := make(chan struct{}) |
| 170 | + sse.Register(api, huma.Operation{ |
| 171 | + OperationID: "sse", |
| 172 | + Method: http.MethodGet, |
| 173 | + Path: "/sse", |
| 174 | + }, map[string]any{ |
| 175 | + "message": &DefaultMessage{}, |
| 176 | + }, func(ctx context.Context, input *struct{}, send sse.Sender) { |
| 177 | + close(started) |
| 178 | + <-release |
| 179 | + }) |
| 180 | + |
| 181 | + w := &orderedWriter{} |
| 182 | + req, _ := http.NewRequest(http.MethodGet, "/sse", nil) |
| 183 | + done := make(chan struct{}) |
| 184 | + go func() { |
| 185 | + api.Adapter().ServeHTTP(w, req) |
| 186 | + close(done) |
| 187 | + }() |
| 188 | + <-started |
| 189 | + // At this point the user handler is running but has not sent any |
| 190 | + // events yet. Headers must already be committed and flushed so |
| 191 | + // that EventSource.onopen fires on the client. |
| 192 | + eventsBeforeRelease := append([]string(nil), w.events...) |
| 193 | + close(release) |
| 194 | + <-done |
| 195 | + |
| 196 | + assert.Equal(t, http.StatusOK, w.status) |
| 197 | + require.Contains(t, eventsBeforeRelease, "writeHeader", |
| 198 | + "WriteHeader must be called before the user handler blocks") |
| 199 | + require.Contains(t, eventsBeforeRelease, "flush", |
| 200 | + "Flush must be called before the user handler blocks") |
| 201 | + whIdx := slices.Index(eventsBeforeRelease, "writeHeader") |
| 202 | + flushIdx := slices.Index(eventsBeforeRelease, "flush") |
| 203 | + assert.Less(t, whIdx, flushIdx, "WriteHeader must precede Flush") |
| 204 | + assert.NotContains(t, eventsBeforeRelease, "write", |
| 205 | + "no body write should occur before the user handler sends an event") |
| 206 | + }, |
| 207 | + }, |
134 | 208 | { |
135 | 209 | Title: "sse stable event order in openapi", |
136 | 210 | TestFunc: func(t *testing.T) { |
|
0 commit comments