fiber icon indicating copy to clipboard operation
fiber copied to clipboard

🐛 [Bug]: Adaptor does not flush when an net/http handler flushes

Open grivera64 opened this issue 8 months ago • 2 comments

Bug Description

The current way the adaptor middleware works, is that it will complete the net/http handler first, then write to a fasthttp response object and return. This makes it incompatible with net/http handlers that may rely on instantly flushing to the client (e.g. Live Data Streams, SSE events).

(Sort of) related to issue #3386.

How to Reproduce

Steps to reproduce the behavior:

  1. Create a net/http handler
func handler(w http.ResponseWriter, r *http.Request) {
	w.Header().Set("Content-Type", "text/plain; charset=utf-8")
	w.Header().Set("Cache-Control", "no-cache")

	bw := bufio.NewWriter(w)

	for i := 0; i < 10; i++ {
		msg := fmt.Sprintf("Line %d\n", i)
		if _, err := bw.WriteString(msg); err != nil {
			fmt.Println("Error writing:", err)
			return
		}
		if err = bw.Flush(); err != nil {
			fmt.Println("Error flushing:", err)
			return
		}
		if flusher, ok := w.(http.Flusher); ok {
			flusher.Flush()
		}
		time.Sleep(1 * time.Second)
	}
}
  1. Initialize a fiber app and use the adaptor middleware to add a route to it.
app := fiber.New()
app.Get("/", adaptor.HTTPHandler(http.HandlerFunc(handler)))
app.Listen(":3000")
  1. Use curl to try to get a response.
curl localhost:3000

At this step, curl will block for 10 seconds before sending the full response, despite the Flush() calls used in the net/http handler.

Expected Behavior

The Adaptor middleware should have the response flush if the net/http handler defines such behavior. The behavior of w.Flush() and flusher.Flush() should mimic the usage of c.SendStreamWriter(streamWriter) if necessary to emit the correct response.

An issue with this is that it is hard to know when a net/http handler is flushing to the client, and treating all requests in this matter would incur a performance cost with flushing to the client on every write (Express does this behavior by default).

While I wouldn't recommend to do this for all requests since fasthttp tries to avoid flushing as much as it can, I would say it is worth having a way to instantly flush data if the net/http handler depends on this behavior.

Fiber Version

v3.0.0-beta.4

Code Snippet (optional)

package main

import (
  "log"

  "github.com/gofiber/fiber/v3"
  "github.com/gofiber/fiber/v3/middleware/adaptor"
)

func main() {
  app := fiber.New()

  // Steps to reproduce
  app.Get("/", adaptor.HTTPHandler(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
    w.Header().Set("Content-Type", "text/plain; charset=utf-8")
    w.Header().Set("Cache-Control", "no-cache")

    bw := bufio.NewWriter(w)
    defer bw.Flush()

    for i := 0; ; i++ {
      msg := fmt.Sprintf("Line %d\n", i)
      _, err := bw.WriteString(msg)
      if err != nil {
        fmt.Println("Error writing:", err)
        return
      }
      err = bw.Flush()
      if err != nil {
        fmt.Println("Error flushing:", err)
        return
      }
      if flusher, ok := w.(http.Flusher); ok {
        flusher.Flush()
      }
      time.Sleep(1 * time.Second)
    }
  }))

  log.Fatal(app.Listen(":3000"))
}

Checklist:

  • [x] I agree to follow Fiber's Code of Conduct.
  • [x] I have checked for existing issues that describe my problem prior to opening this one.
  • [x] I understand that improperly formatted bug reports may be closed without explanation.

grivera64 avatar Apr 22 '25 17:04 grivera64

@grivera64 I notice this too, I don't think that's the use case of the adaptor. It's more for things that only support net/http and you want a quick(with overhead) way of accessing/exposing.

Probably worth investigating though

gaby avatar Apr 24 '25 00:04 gaby

In my opinion, maybe the problem is caused by fasthttp.

package main

import (
	"bufio"
	"fmt"
	"net/http"
	"time"

	"github.com/valyala/fasthttp"
	"github.com/valyala/fasthttp/fasthttpadaptor"
)

func main() {
	handler := http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
		w.Header().Set("Content-Type", "text/plain; charset=utf-8")
		w.Header().Set("Cache-Control", "no-cache")

		bw := bufio.NewWriter(w)
		defer bw.Flush()

		for i := 0; ; i++ {
			msg := fmt.Sprintf("Line %d\n", i)
			_, err := bw.WriteString(msg)
			if err != nil {
				fmt.Println("Error writing:", err)
				return
			}
			err = bw.Flush()
			if err != nil {
				fmt.Println("Error flushing:", err)
				return
			}
			if flusher, ok := w.(http.Flusher); ok {
				flusher.Flush()
			}
			time.Sleep(1 * time.Second)
		}
	})
	fastHTTPHandler := fasthttpadaptor.NewFastHTTPHandler(handler)

	go http.ListenAndServe(":3001", handler)
	fasthttp.ListenAndServe(":3000", fastHTTPHandler)
}

No response (fasthttp)

> curl localhost:3000

Response per seconds (net/http)

> curl localhost:3000
Line 0
Line 1
Line 2
Line 3
Line 4
Line 5

ksw2000 avatar Jun 04 '25 12:06 ksw2000

In my opinion, maybe the problem is caused by fasthttp.

I submitted a PR to add flushing support for fasthttpadaptor in the fasthttp repo. Once merged, we can bump to the next release and close this issue.

grivera64 avatar Aug 11 '25 21:08 grivera64

@grivera64 We are waiting on 1.66.0 for this right?

gaby avatar Sep 19 '25 12:09 gaby

@grivera64 We are waiting on 1.66.0 for this right?

@gaby Yes, after we address the failing workflow in #3741 with the upstream PR https://github.com/valyala/fasthttp/pull/2069, this should be fixed for the majority of cases.

The last case I see that could still have issues is when using a net/http handler as middleware. If a net/http middleware uses Flush() or Hijack(), then any other matched handlers currently would either:

  • Fail to send a response
  • Override the response

I think this limitation is similar to using SendStreamWriter() in a middleware in v3. All SendXXX() methods are mutually exclusive, and using several of these across several middleware has undefined behavior. (Their implementation within fasthttp will have one take precedence over the other and close the connection.)

Would you say that this is expected behavior rather than a bug for the middleware case?

grivera64 avatar Sep 19 '25 21:09 grivera64

@gaby Since #3790 was successfully merged and v1.67.0 has the added flushing functionality, should we now add unit tests to verify that the adaptor middleware flushing functionality is fully working?

grivera64 avatar Oct 09 '25 19:10 grivera64

There's a related issue with adaptor.FiberHandler, where it does not support buffered streams. Currently, it waits for the entire stream writer to finish writing, then sends the full response.

I can make a separate issue and submit a PR to fix it.

grivera64 avatar Oct 09 '25 23:10 grivera64

@gaby Since #3790 was successfully merged and v1.67.0 has the added flushing functionality, should we now add unit tests to verify that the adaptor middleware flushing functionality is fully working?

Yes 💪

gaby avatar Oct 10 '25 11:10 gaby