fix(sse): flush response headers before blocking select loop
Without flusher.Flush() after WriteHeader, the HTTP response headers were buffered and never sent to the client, causing SSE clients to hang indefinitely waiting for the initial response. - Add flusher.Flush() after WriteHeader in HandleSSE - Add TestSSEStreamReceivesEventOnCapture integration test: starts test server, subscribes to SSE, captures a request, and verifies the event arrives within 2s Co-Authored-By: Paperclip <noreply@paperclip.ing>
This commit is contained in:
parent
4256d34c27
commit
cf5b2f185c
@ -8,6 +8,7 @@ import (
|
|||||||
"net/http/httptest"
|
"net/http/httptest"
|
||||||
"strings"
|
"strings"
|
||||||
"testing"
|
"testing"
|
||||||
|
"time"
|
||||||
|
|
||||||
"github.com/elfoundation/hatch/internal/store"
|
"github.com/elfoundation/hatch/internal/store"
|
||||||
"github.com/go-chi/chi/v5"
|
"github.com/go-chi/chi/v5"
|
||||||
@ -129,6 +130,80 @@ func TestCaptureRecordsAllVerbs(t *testing.T) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func TestSSEStreamReceivesEventOnCapture(t *testing.T) {
|
||||||
|
repo := newFakeRepo()
|
||||||
|
srv := httptest.NewServer(testRouter(repo))
|
||||||
|
defer srv.Close()
|
||||||
|
|
||||||
|
// Use separate clients with separate transports to avoid
|
||||||
|
// connection-pool contention with the long-lived SSE connection.
|
||||||
|
sseClient := &http.Client{
|
||||||
|
Transport: &http.Transport{DisableKeepAlives: true},
|
||||||
|
}
|
||||||
|
captureClient := &http.Client{
|
||||||
|
Transport: &http.Transport{DisableKeepAlives: true},
|
||||||
|
}
|
||||||
|
|
||||||
|
// Subscribe to SSE with a cancellable context.
|
||||||
|
ctx, cancel := context.WithCancel(context.Background())
|
||||||
|
defer cancel()
|
||||||
|
|
||||||
|
sseReq, err := http.NewRequestWithContext(ctx, http.MethodGet, srv.URL+"/e/ep/events", nil)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("SSE request create failed: %v", err)
|
||||||
|
}
|
||||||
|
sseResp, err := sseClient.Do(sseReq)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("SSE GET failed: %v", err)
|
||||||
|
}
|
||||||
|
defer sseResp.Body.Close()
|
||||||
|
|
||||||
|
if sseResp.StatusCode != http.StatusOK {
|
||||||
|
t.Fatalf("SSE returned %d", sseResp.StatusCode)
|
||||||
|
}
|
||||||
|
ct := sseResp.Header.Get("Content-Type")
|
||||||
|
if !strings.HasPrefix(ct, "text/event-stream") {
|
||||||
|
t.Fatalf("SSE Content-Type is %q, expected text/event-stream", ct)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Read SSE body in a goroutine; capture in main goroutine.
|
||||||
|
bodyCh := make(chan string, 1)
|
||||||
|
go func() {
|
||||||
|
buf := make([]byte, 4096)
|
||||||
|
n, _ := sseResp.Body.Read(buf)
|
||||||
|
bodyCh <- string(buf[:n])
|
||||||
|
}()
|
||||||
|
|
||||||
|
// Small sleep to let the SSE subscription register.
|
||||||
|
time.Sleep(10 * time.Millisecond)
|
||||||
|
|
||||||
|
// Capture a request on the same server.
|
||||||
|
captureResp, err := captureClient.Post(srv.URL+"/ep", "application/json", strings.NewReader(`{"msg":"hello"}`))
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("capture POST failed: %v", err)
|
||||||
|
}
|
||||||
|
captureResp.Body.Close()
|
||||||
|
if captureResp.StatusCode != http.StatusOK {
|
||||||
|
t.Fatalf("capture returned %d", captureResp.StatusCode)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Wait for the SSE event with a timeout.
|
||||||
|
select {
|
||||||
|
case body := <-bodyCh:
|
||||||
|
if !strings.Contains(body, "data:") {
|
||||||
|
t.Fatalf("SSE stream missing 'data:' prefix: %q", body)
|
||||||
|
}
|
||||||
|
if !strings.Contains(body, "\"method\":\"POST\"") {
|
||||||
|
t.Fatalf("SSE stream missing POST method: %q", body)
|
||||||
|
}
|
||||||
|
case <-time.After(2 * time.Second):
|
||||||
|
t.Fatal("timeout waiting for SSE event")
|
||||||
|
}
|
||||||
|
|
||||||
|
// Cancel context to close the SSE connection.
|
||||||
|
cancel()
|
||||||
|
}
|
||||||
|
|
||||||
func TestCaptureReturnsJSON200(t *testing.T) {
|
func TestCaptureReturnsJSON200(t *testing.T) {
|
||||||
r := testRouter(newFakeRepo())
|
r := testRouter(newFakeRepo())
|
||||||
w := httptest.NewRecorder()
|
w := httptest.NewRecorder()
|
||||||
|
|||||||
@ -80,6 +80,7 @@ func HandleSSE(repo store.Repository) http.HandlerFunc {
|
|||||||
w.Header().Set("Cache-Control", "no-cache")
|
w.Header().Set("Cache-Control", "no-cache")
|
||||||
w.Header().Set("Connection", "keep-alive")
|
w.Header().Set("Connection", "keep-alive")
|
||||||
w.WriteHeader(http.StatusOK)
|
w.WriteHeader(http.StatusOK)
|
||||||
|
flusher.Flush()
|
||||||
|
|
||||||
ch := hub.subscribe(endpointID)
|
ch := hub.subscribe(endpointID)
|
||||||
defer hub.unsubscribe(endpointID, ch)
|
defer hub.unsubscribe(endpointID, ch)
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user