Files
zip/adapt_stream_test.go
zeekayandHanzo Dev 5e3108efb2 a mounted handler streams instead of being collected
AdaptNetHTTP handed the stdlib handler fasthttp's adaptor writer, which gathers
the whole body and copies it out when the handler returns. For an ordinary reply
that is only a buffer; for server-sent events it is the difference between a
stream and one delivery at the end, so a caller watching tokens arrive saw
nothing until the answer was already complete.

The handler now runs against a pipe, and the pipe is synchronous: a Write does
not return until the reader has taken the bytes, which is what makes Flush
honest here rather than a stub. Status and headers are read from the handler's
first act and applied before the body streams, because fasthttp writes them once
and cannot revise them after.

Tested over a real socket rather than the in-memory helper — that helper collects
the whole response, so it cannot tell a stream from a buffer, which is the one
distinction these tests exist to make.

Co-authored-by: Hanzo Dev <[email protected]>
2026-08-21 01:20:24 -07:00

160 lines
4.8 KiB
Go

package zip
import (
"bufio"
"fmt"
"io"
"net"
"net/http"
"strings"
"testing"
"time"
"github.com/gofiber/fiber/v3"
)
// serve starts the app on a real socket. A real socket is the point: fiber's
// in-memory Test helper collects the whole response, so it cannot tell a stream
// apart from a buffer, which is the exact distinction these tests exist to make.
func serve(t *testing.T, app *App) string {
t.Helper()
ln, err := net.Listen("tcp", "127.0.0.1:0")
if err != nil {
t.Fatal(err)
}
go func() { _ = app.fiber.Listener(ln, fiber.ListenConfig{DisableStartupMessage: true}) }()
t.Cleanup(func() { _ = app.fiber.Shutdown() })
for i := 0; i < 100; i++ { // wait for accept
c, err := net.DialTimeout("tcp", ln.Addr().String(), 50*time.Millisecond)
if err == nil {
_ = c.Close()
break
}
time.Sleep(10 * time.Millisecond)
}
return "http://" + ln.Addr().String()
}
func newApp() *App { return New(Config{DisableStartupMessage: true}) }
// THE ASSERTION THAT WAS FAILING. Every streaming handler in a mounted stdlib
// server begins by asking whether it may flush; the buffering adaptor answered
// no, and the handler returned 500 instead of a stream.
func TestAdaptNetHTTP_WriterIsAFlusher(t *testing.T) {
app := newApp()
app.All("/x/*", AdaptNetHTTP(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
if _, ok := w.(http.Flusher); !ok {
http.Error(w, "writer does not implement http.Flusher", http.StatusInternalServerError)
return
}
fmt.Fprint(w, "flushable")
})))
res, err := http.Get(serve(t, app) + "/x/y")
if err != nil {
t.Fatal(err)
}
defer res.Body.Close()
body, _ := io.ReadAll(res.Body)
if res.StatusCode != 200 || string(body) != "flushable" {
t.Fatalf("status %d body %q — the mounted handler could not flush", res.StatusCode, body)
}
}
// And it must actually STREAM: a frame written before the handler returns has to
// be readable before it returns, or "streaming" is buffering that compiles.
func TestAdaptNetHTTP_FramesArriveBeforeTheHandlerReturns(t *testing.T) {
release := make(chan struct{})
app := newApp()
app.All("/sse/*", AdaptNetHTTP(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
w.Header().Set("Content-Type", "text/event-stream")
fmt.Fprint(w, "data: first\n\n")
w.(http.Flusher).Flush()
<-release // the handler is still running
fmt.Fprint(w, "data: second\n\n")
})))
res, err := http.Get(serve(t, app) + "/sse/x")
if err != nil {
t.Fatal(err)
}
defer res.Body.Close()
if ct := res.Header.Get("Content-Type"); !strings.Contains(ct, "text/event-stream") {
t.Fatalf("content-type %q — headers must be set before the body streams", ct)
}
done := make(chan string, 1)
go func() {
line, _ := bufio.NewReader(res.Body).ReadString('\n')
done <- line
}()
select {
case line := <-done:
if !strings.Contains(line, "first") {
t.Fatalf("first frame %q", line)
}
case <-time.After(3 * time.Second):
close(release)
t.Fatal("nothing arrived while the handler was still running — still buffering")
}
close(release)
}
// The ordinary case must not regress.
func TestAdaptNetHTTP_StatusAndHeadersSurvive(t *testing.T) {
app := newApp()
app.All("/j/*", AdaptNetHTTP(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
w.Header().Set("Content-Type", "application/json")
w.Header().Set("X-Trace", "abc")
w.WriteHeader(http.StatusTeapot)
fmt.Fprint(w, `{"ok":1}`)
})))
res, err := http.Get(serve(t, app) + "/j/k")
if err != nil {
t.Fatal(err)
}
defer res.Body.Close()
body, _ := io.ReadAll(res.Body)
if res.StatusCode != http.StatusTeapot {
t.Fatalf("status %d, want 418", res.StatusCode)
}
if res.Header.Get("X-Trace") != "abc" || !strings.Contains(res.Header.Get("Content-Type"), "application/json") {
t.Fatalf("headers lost: %v", res.Header)
}
if string(body) != `{"ok":1}` {
t.Fatalf("body %q", body)
}
}
// A handler that writes nothing still has to answer.
func TestAdaptNetHTTP_SilentHandlerStillAnswers(t *testing.T) {
app := newApp()
app.All("/q/*", AdaptNetHTTP(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {})))
res, err := http.Get(serve(t, app) + "/q/z")
if err != nil {
t.Fatal(err)
}
defer res.Body.Close()
if res.StatusCode != 200 {
t.Fatalf("status %d", res.StatusCode)
}
}
// A panicking handler must close the stream rather than strand the reader.
func TestAdaptNetHTTP_PanicClosesTheStream(t *testing.T) {
app := newApp()
app.All("/p/*", AdaptNetHTTP(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
fmt.Fprint(w, "partial")
panic("boom")
})))
res, err := http.Get(serve(t, app) + "/p/x")
if err != nil {
t.Fatal(err)
}
defer res.Body.Close()
done := make(chan struct{})
go func() { _, _ = io.ReadAll(res.Body); close(done) }()
select {
case <-done:
case <-time.After(3 * time.Second):
t.Fatal("reader stranded after the handler panicked")
}
}