Search code examples
gogo-http

Unable to detect client exit in GO with net/http when not on same machine


I am trying to make an application that periodically sends data to a client's browser, unitl the client's browser closes. My problem is, that I am unable to detect client closing the browser.

I have following code writen in GO (minimal code where the problem is pressent):

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

func test(w http.ResponseWriter, r *http.Request) {
    f, f_err := w.(http.Flusher)
    if !f_err {
        fmt.Printf("%s\n", "flush error")
        return
    }
    w.Header().Set("Cache-Control", "no-cache")
    w.Header().Set("Content-Type", "text/event-stream")
    w.WriteHeader(http.StatusOK)
    for timeout := 0; timeout < 1000; timeout++ {
        writ, err := w.Write([]byte("data: {\"result\": \"success\"}\n\n"))
        f.Flush()
        fmt.Printf("%d %d %s\n", timeout, writ, err)
        time.Sleep(time.Duration(1) * time.Second)
    }
}

func main() {
    mux := http.NewServeMux()
    mux.HandleFunc("/test", func(w http.ResponseWriter, r *http.Request) {
        test(w, r)
    })
    srv := &http.Server{
        Addr:    ":3001",
        Handler: mux,
    }
    err := srv.ListenAndServe()
    if err != nil {
        panic(err)
    }
}

The code above is my server and I connect to this server via ssh tunel (from port 3001 to port 3001) with:

$ curl localhost:3001/test

When I kill the curl, the server is still trying to send data. This results in server overload after some time.

This is what is happening on client and server:

Server:

$ ./test
0 29 %!s(<nil>)
1 29 %!s(<nil>)
2 29 %!s(<nil>)
3 29 %!s(<nil>)
4 29 %!s(<nil>)
5 29 %!s(<nil>)
6 29 %!s(<nil>)
7 29 %!s(<nil>)
...
...
...

Client:

$ curl localhost:3001/test
data: {"result": "success"}

data: {"result": "success"}

data: {"result": "success"}

data: {"result": "success"}

^C

What I want to achive is the ability to detect kill of curl.

When I try to run and later kill curl on same machine where server is running, I end up with:

Server:

$ ./test
0 29 %!s(<nil>)
1 29 %!s(<nil>)
2 29 %!s(<nil>)
3 29 %!s(<nil>)
4 29 %!s(<nil>)
5 29 %!s(<nil>)
6 29 %!s(<nil>)
7 0 write tcp 127.0.0.1:3001->127.0.0.1:51058: write: broken pipe
8 0 write tcp 127.0.0.1:3001->127.0.0.1:51058: write: broken pipe
9 0 write tcp 127.0.0.1:3001->127.0.0.1:51058: write: broken pipe
10 0 write tcp 127.0.0.1:3001->127.0.0.1:51058: write: broken pipe
11 0 write tcp 127.0.0.1:3001->127.0.0.1:51058: write: broken pipe
12 0 write tcp 127.0.0.1:3001->127.0.0.1:51058: write: broken pipe
13 0 write tcp 127.0.0.1:3001->127.0.0.1:51058: write: broken pipe
...
...
...

Client:

$ curl localhost:3001/test                                                                                                                             
data: {"result": "success"}

data: {"result": "success"}

data: {"result": "success"}

data: {"result": "success"}

^C

This is exactly somethnig I would like to have, when I am using the ssh tunnel.


Solution

  • According to this article (Section About streaming). Go net/http cannot detect ungraceful disconnects. Therefore, connection over SSH tunnel, lost wifi, unplugged cable, browser crash, etc., cannot be detected with net/http.

    Note: This may change in the future since underlying net.Conn is capable of detecting these types of error.