Blog by Simon Frey

Manual flush golang http.ResponseWriter

Problem

For a recent project I wanted to start rendering the HTML page even if the server was still working on a long runing task. (For showing the loading screen of https://unshort.link without the use of JavaScript)

My code looked basically like following:

func h(rw http.ResponseWriter, req *http.Request) {
    io.Copy(rw,loadingHTMLByteReader)   
    tResult = performLongRunningTask()
    rw.Write(tResult)
}

loadingHTMLByteReader is the HTML I already wanted to render even that the server can not yet write all results.

But sadly that did not produce the result I anticipated. The page was still rendered completely at once after the complete process was done.
Apparently go buffers there response writer until the handler returns, or the buffer (default 4KB) is full.

The buffer is defined via the http.Transport:

type Transport struct {
    ...
    // WriteBufferSize specifies the size of the write buffer used
    // when writing to the transport.
    // If zero, a default (currently 4KB) is used.
    WriteBufferSize int
    ...
}

Ideas

So there are two options for overarching my intended result:

  1. set WriteBufferSize to something really small
  2. manual flush the http.ResponseWriter buffer

Option 1 is obviously a unsatisfying solution, as I would not be able to control exactly that the complete loadingHTMLByteReader would be send if it overlaps the buffer borders.

E.g. loadingHTMLByteReader is of size 3KB if the buffer size is set to 2KB, the last 2KB would not have been written until the buffer is filled with additional data or the handler returned. So I went for option 2

How to flush http.ResponseWriter

Researching the net I found the following solution how to manually flush the http.ResponseWriter:

if f, ok := rw.(http.Flusher); ok {
    f.Flush()
}

My problem is solved and the site works as intended:

func h(rw http.ResponseWriter, req *http.Request) {
    io.Copy(rw,loadingHTMLByteReader)
    if f, ok := rw.(http.Flusher); ok {
        f.Flush()
    }
    tResult = performLongRunningTask()
    rw.Write(tResult)
}

Important notes

The http.ResponseWriter not always implements the Flusher interface

As stated in the godoc the ResponseWriter is not always implementing the Flusher interface, especially when you using wrappers around the ResponseWriter.
It is important to always check if the type assert worked and only then use the Flusher to prevent server panics in edge cases

There is other buffering in the network

Flushing the response writer only has control about the application layer buffering. You have no control over what the operating system, the network and the client does. All of these entities may introduce additional caching themselves.
Using the manual flush should only be used for performance improvements and nice to have features. Do not assume you can use it for any process critical behavior, it’s only a nice to have.

Sources:
https://stackoverflow.com/questions/19292113/not-buffered-http-responsewritter-in-golang