Files
zeekayandHanzo Dev 550e1fe230 a mounted handler streams instead of being collected
AdaptNetHTTP handed the stdlib handler fiber'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 — a caller watching tokens arrive sees
nothing until the answer is already complete, which looks exactly like a slow
model and is why "streaming works" could be true and useless at the same time.

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 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 afterwards.

Tested over a real socket, not the in-memory helper — that helper collects the
whole response, so it cannot tell a stream from a buffer.

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

65 lines
2.7 KiB
Go

// Adapters for bringing existing net/http code onto zip
// without rewriting handlers. These are MIGRATION TOOLS — each adapter
// costs ~5% perf vs native Fiber dispatch and exists so a service can
// roll onto zip incrementally. Replace adapted routes with native
// zip handlers when feasible.
package zip
import (
"net/http"
"github.com/zap-proto/fiber/v3/middleware/adaptor"
)
// AdaptNetHTTP wraps an http.Handler so it can be served on a zip router as an
// ordinary zip.Handler. To front a whole foreign subtree, take a Group for the
// prefix and register a wildcard on it — this is THE way to bring net/http code
// onto zip:
//
// app.Group("/legacy").All("/*", zip.AdaptNetHTTP(httpHandler))
//
// Group is what carries the prefix; Proxy does not do this. Proxy delegates a
// prefix to a REMOTE address over a transport, so it takes a string, not a
// handler. A local handler is composed, not delegated.
//
// The wildcard stays an ordinary route, so most-specific-wins precedence still
// applies: a route registered on the group afterwards still beats it for its
// exact path.
//
// g := app.Group("/legacy")
// g.All("/*", zip.AdaptNetHTTP(httpHandler)) // everything else
// g.Get("/health", nativeHealth) // still wins
//
// A bare func is adapted through http.HandlerFunc, which is itself an
// http.Handler — there is no separate func-shaped adapter:
//
// app.Get("/legacy", zip.AdaptNetHTTP(http.HandlerFunc(myFunc)))
//
// Migration tool — costs ~5% perf vs native Fiber. Replace with native
// zip handlers when feasible.
func AdaptNetHTTP(h http.Handler) Handler {
// Streams — see adapt_stream.go. ONE adapter rather than a buffering one and
// a streaming one, because "which adapter did this route get" is exactly the
// question nobody should have to ask to explain why server-sent events
// arrive all at once on one mount and incrementally on another.
//
// TERMINAL: an http.Handler writes the response, so this answers whatever
// address it is registered at. In Use position it would answer every one of
// them, which is why the wildcard route above is THE spelling and this is
// refused at build time — see [Terminal].
return Terminal("zip.AdaptNetHTTP", adaptStreaming(h))
}
// AdaptNetHTTPMiddleware wraps a stdlib middleware (func(http.Handler) http.Handler)
// as a zip.Handler — the net/http-middleware bridge. Because it IS middleware,
// it goes on a Group's Use rather than on a route:
//
// app.Group("/legacy").Use(zip.AdaptNetHTTPMiddleware(mw))
//
// Migration tool — costs ~5% perf vs native Fiber.
func AdaptNetHTTPMiddleware(mw func(http.Handler) http.Handler) Handler {
wrapped := adaptor.HTTPMiddleware(mw)
return func(c *Ctx) error { return wrapped(c.fc) }
}