1

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.

3
  • 1
    What happens if you add select { case <- r.Context().Done(): panic("client closed"); default: ;} somewhere before the w.Write() call? Does it eventually panic? The reason behind my question is that you're trying to detect the disconnected client in a somewhat weird way: net/http stack is supposed to tell you the client of a request is gone using that request's context. So I'd first try to detect client disconnects using "the documented way" to deal with them. Commented Aug 8, 2024 at 12:41
  • 2
    Even if you don't detect the client disconnect right way, you will eventually get write errors, and you can see from your server output you are getting an error and choosing to continue anyway, Commented Aug 8, 2024 at 13:35
  • Adding select { case <- r.Context().Done(): panic("client closed"); default: ;} before w.Write() did not helped. Commented Aug 12, 2024 at 11:37

2 Answers 2

2

To detect client disconnects, check for request context cancelation and write errors.

func test(w http.ResponseWriter, r *http.Request) {
    rc := http.NewResponseController(w)
    w.Header().Set("Cache-Control", "no-cache")
    w.Header().Set("Content-Type", "text/event-stream")
    w.WriteHeader(http.StatusOK)
    t := time.NewTicker(time.Second)
    defer t.Stop()
    for timeout := 0; timeout < 1000; timeout++ {
        select {
        case <-t.C:
            _, err := w.Write([]byte("data: {\"result\": \"success\"}\n\n"))
            if err != nil {
                fmt.Println("write err", err)
                return // Exit on write error.
            }
            err = rc.Flush()
            if err != nil {
                fmt.Println("flush err", err)
                return // Exit on write error.
            }
        case <-r.Context().Done():
            fmt.Println("context canceled", r.Context().Err())
            return // Exit on context cancelation.
        }
    }
}
Sign up to request clarification or add additional context in comments.

1 Comment

This works when the request is made on the same machine. It does not work when the connection goes through an SSH tunnel.
0

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.

UPDATE: I just retried the experiment and it now works. You get error write: broken pipe.

1 Comment

Very interesting, thanks.

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Start asking to get answers

Find the answer to your question by asking.

Ask question

Explore related questions

See similar questions with these tags.