How to make an endpoint to download data with browser using Go

Francesco Pastore
2 min readMay 16, 2021

Lately at work, I needed to make an endpoint to download data. The problem was that the files were stored on a very slow file server and the simple HTTP request often went on timeout. Fortunately, there is a simple solution using the utility flusher of package HTTP!

In this article, we will create a simple example using the interface flusher of the official HTTP Go module. In particular, we will see how to buffer response and make the browser directly downloads data.

The main function

First of all, take a look at the main function:

func main() {
http.HandleFunc("/", index)
http.HandleFunc("/download", download)
log.Println("Server is listening on port 8080") err := http.ListenAndServe(":8080", nil)
if err != nil {
log.Fatal(err)
}
}

It is a simple http server with two endpoints.

The index function

The index function is a simple HTTP handler that replies with an HTML page with a clickable link to start the download.

func index(w http.ResponseWriter, r *http.Request) {
w.Header().Add("Content-Type", "text/html")
w.Write([]byte(`
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta http-equiv="X-UA-Compatible" content="IE=edge">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Document</title>
</head>
<body>
<a href="http://localhost:8080/download">
Click me!
</a>
</body>
</html>
`))
}

The download function

In the download function, we can see the main logic of this example.
We will reply with a text files with some delay on each word.

text := "This file is very big!"                        
words := strings.Split(text, " ")

We need to set some headers to tell the browser that the response is a download to be handled.

w.Header().Add("Content-Type", "text/plain")
w.Header().Add("Content-Disposition", "attachment; filename=words.txt")
w.Header().Add("Content-Length", fmt.Sprint(len(text)))

Then, we can start the buffered response by creating the flusher, add the first word in it and flush every time we want to send some data to the client.
The sleep call is used to emulate a possible delay, like external requests to other servers.
Working with flusher, it is very important to understand that some browsers, like Firefox, wait for at least a bit before starting the download. So, the flusher must be called at least one time with some data in it to make the download start.

flusher, ok := w.(http.Flusher)
if !ok {
log.Fatal("Cannot use flusher")
}
w.Write([]byte(words[0]))
flusher.Flush()
for i := 1; i < len(words); i++ {
time.Sleep(5 * time.Second)
w.Write([]byte(" " + words[i]))
flusher.Flush()
}

The complete download function:

func download(w http.ResponseWriter, r *http.Request) { text := "This file is very big!"
words := strings.Split(text, " ")
w.Header().Add("Content-Type", "text/plain")
w.Header().Add("Content-Disposition", "attachment; filename=words.txt")
w.Header().Add("Content-Length", fmt.Sprint(len(text)))
flusher, ok := w.(http.Flusher)
if !ok {
log.Fatal("Cannot use flusher")
}
w.Write([]byte(words[0]))
flusher.Flush()
for i := 1; i < len(words); i++ {
time.Sleep(5 * time.Second)
w.Write([]byte(" " + words[i]))
flusher.Flush()
}
}

Final result

--

--

Francesco Pastore

An engineering student in Milan and a web developer for an IT company. Write about programming and cybersecurity topics.