Skip to content

Instantly share code, notes, and snippets.

@harshavardhana
Created September 12, 2020 23:28
Show Gist options
  • Save harshavardhana/c51dcfd055780eaeb71db54f9c589150 to your computer and use it in GitHub Desktop.
Save harshavardhana/c51dcfd055780eaeb71db54f9c589150 to your computer and use it in GitHub Desktop.
package main
import (
"fmt"
"log"
"net"
"net/http"
"runtime"
"time"
"net/http/pprof"
"github.com/gorilla/mux"
)
func attachProfiler(router *mux.Router) {
router.HandleFunc("/debug/pprof/", pprof.Index)
router.HandleFunc("/debug/pprof/cmdline", pprof.Cmdline)
router.HandleFunc("/debug/pprof/profile", pprof.Profile)
router.HandleFunc("/debug/pprof/symbol", pprof.Symbol)
// Manually add support for paths linked to by index page at /debug/pprof/
router.Handle("/debug/pprof/goroutine", pprof.Handler("goroutine"))
router.Handle("/debug/pprof/heap", pprof.Handler("heap"))
router.Handle("/debug/pprof/threadcreate", pprof.Handler("threadcreate"))
router.Handle("/debug/pprof/block", pprof.Handler("block"))
}
func basicHandler(w http.ResponseWriter, r *http.Request) {
switch r.Method {
case http.MethodGet:
<-r.Context().Done()
fmt.Println(r.URL, r.Method)
case http.MethodPut:
fmt.Println("Number of routines", runtime.NumGoroutine())
<-r.Context().Done()
fmt.Println(r.URL, r.Method)
default:
fmt.Fprintf(w, "Sorry, only POST methods are supported.")
}
}
func main() {
router := mux.NewRouter().SkipClean(true).UseEncodedPath()
attachProfiler(router)
router.Methods(http.MethodGet).HandlerFunc(basicHandler)
router.Methods(http.MethodPut).HandlerFunc(basicHandler)
s := &http.Server{
Addr: ":4222",
Handler: router,
ReadHeaderTimeout: 2 * time.Second,
IdleTimeout: 10 * time.Second,
MaxHeaderBytes: 1 << 20,
ConnState: func(c net.Conn, state http.ConnState) {
fmt.Println("ConnState", c, state)
},
}
if err := s.ListenAndServe(); err != nil {
log.Fatal(err)
}
}
@harshavardhana
Copy link
Author

I found an interesting problem with Go net/http server which affects us quite dramatically, especially for the operations with HTTP body, Was trying to debug if r.Context().Done() always closes the connection as per https://golang.org/pkg/net/http/#Request.Context

For incoming server requests, the context is canceled when the client's connection closes, the request is canceled (with HTTP/2), or when the ServeHTTP method returns.

Seems like this is not true always - this I tested is true for all calls which do not have a body i.e when client is not sending any data.

  • HEAD, GET, DELETE, OPTIONS - r.Context().Done() returns when client closes connection.
  • PUT, POST both with body do not seem to receive r.Context().Done() even if the client closes connection.

This is because I found that the only way net/http knows that client has closed connection is when we read from r.Body() and is not always true for all files for example.

PUT/POST with 0byte body behaves differently than PUT/POST even with 1byte body.

Now, this actually causes quite a lot of challenges for us, because we are relying on ServeHTTP() received context to cancel rouge clients, Which I see is that we cannot rely upon if the client closed the connection without we starting to read the body. You should see that r.Context().Done() is never hit until r.Read() hits EOF/UnexpectedEOF i.e client prematurely closing the connection.

So in essence this is a HTTP1.1 issue from what I can read from the Go source as they cannot provide this type of functionality themselves, and it is up to us to read the body always as soon as possible.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment