🪠 Handle Broken Pipe error in Go

http errors

The broken pipe is an error occurring in Go when you write to a stream where the other end (the peer) has closed the underlying connection. It can usually be seen in an HTTP server that returns a large size response. If the server client interrupts the connection in the middle of retrieving the response, then the server reports the write: broken pipe from the sending data function.

Broken pipe example

In the following example, we will create a simple server to simulate the broken pipe error.

package main

import (
    "errors"
    "fmt"
    "io"
    "log"
    "net/http"
    "syscall"
    "time"
)

type flushWriter struct {
    f http.Flusher
    w io.Writer
}

func (fw *flushWriter) Write(p []byte) (n int, err error) {
    n, err = fw.w.Write(p)
    if fw.f != nil {
        fw.f.Flush()
    }
    return
}

func slowServer(w http.ResponseWriter, r *http.Request) {
    fw := flushWriter{w: w}
    if f, ok := w.(http.Flusher); ok {
        fw.f = f
    }

    msg := "Hello World!"
    // write msg rune by rune
    for _, r := range msg {
        _, err := fw.Write([]byte(string(r)))
        if errors.Is(err, syscall.EPIPE) {
            log.Printf("found BROKEN PIPE error:\n%v", err)
        } else if err != nil {
            log.Print(err)
        }

        fmt.Println(string(r))
        time.Sleep(2 * time.Second)
    }
}

func main() {
    // run slow server
    http.HandleFunc("/", slowServer)

    if err := http.ListenAndServe(":8080", nil); err != nil {
        log.Fatal(err)
    }
}

How the server works

The slowServer handler returns a "Hello World!" message character by character in response to client requests. We want the handler to be slow to have a chance to close the connection on the client side, so each character of the message is written every 2 seconds. To write the response, we use the flushWriter which is a wrapper for http.ResponseWriter, and its job is to flush the data buffered by the http.ResponseWriter to the client, right after every Write(). Why is it necessary? The standard write buffer size is 4 KB, so without the flushing, the server sends the data to the client only once, after the handler code finishes. But to detect broken pipe error we need at least 2 writes to the client (this is how TCP sockets work), so in the server handler, we ensure that we have more than one write by disabling buffering.

Run the example

To trigger the broken pipe error, we need to:

  1. Run our server code, for example by calling go run main.go in the terminal.
  2. Run the client that gets data from the server. We can do this using the curl command in the terminal:
curl --request GET --url http://localhost:8080/
  1. Interrupt the curl command before the end of fetching the data by pressing Ctrl+C.
  2. After two more writes, we start getting write: broken pipe errors as a result of the flushWriter.Write() method:
H
e
l
l
2021/09/16 12:50:57 found BROKEN PIPE error:
write tcp 127.0.0.1:8080->127.0.0.1:62751: write: broken pipe
o
2021/09/16 12:50:59 found BROKEN PIPE error:
write tcp 127.0.0.1:8080->127.0.0.1:62751: write: broken pipe
 
2021/09/16 12:51:01 found BROKEN PIPE error:
write tcp 127.0.0.1:8080->127.0.0.1:62751: write: broken pipe
W
2021/09/16 12:51:03 found BROKEN PIPE error:
write tcp 127.0.0.1:8080->127.0.0.1:62751: write: broken pipe
o
2021/09/16 12:51:05 found BROKEN PIPE error:
write tcp 127.0.0.1:8080->127.0.0.1:62751: write: broken pipe
r
2021/09/16 12:51:07 found BROKEN PIPE error:
write tcp 127.0.0.1:8080->127.0.0.1:62751: write: broken pipe
l
2021/09/16 12:51:09 found BROKEN PIPE error:
write tcp 127.0.0.1:8080->127.0.0.1:62751: write: broken pipe
d
2021/09/16 12:51:11 found BROKEN PIPE error:
write tcp 127.0.0.1:8080->127.0.0.1:62751: write: broken pipe
!

Handle broken pipe error

To handle the broken pipe, you need to check if the error returned from the HTTP writer Write() method is an EPIPE system error. In the example above, we perform this check to print an additional message, along with the broken pipe. Typically, when the error happens on the server side, there is no need to handle it in any special way. You can log it or ignore it altogether since it is normal practice in the TCP protocol for clients to close connections.

📡 Handle Context Deadline Exceeded error in Go

Learn how to check if a HTTP client returns a request timeout error
http errors

🕵️ Solve 'cannot take address of XXX' error in Go

Learn how to take the address of a literal, map value, or function return value
introduction pointer errors

📂 Check if a file exists in Go

Learn how to check if a file exists in Go after or before opening it
introduction file errors