http.DefaultClient

“1024 lashes for anyone using http.DefaultClient in their code” — that’s the kind of “joke” you hear at our standups. The problem is that the defaults don’t set any timeouts. There’s a great article on the topic on the Cloudflare blog1.

We’ll try to keep http.DefaultClient from being used in our code at all. There are at least two tools for this:

For the experiments we’ll need a sample piece of code:

package main

import (
	"net/http"

	. "net/http"
)

func main() {
	_ = http.DefaultClient
	_ = DefaultClient

	http.Get("url")
	Get("url")

	http.Post("url", "application/bar", nil)
	Post("url", "application/bar", nil)

	http.PostForm("url", nil)
	PostForm("url", nil)

	http.ListenAndServe(":1337", nil)
	ListenAndServe(":1337", nil)

	http.Handle("/", http.HandlerFunc(func(http.ResponseWriter, *http.Request) {}))
	Handle("/", http.HandlerFunc(func(http.ResponseWriter, *http.Request) {}))

	http.HandleFunc("/", func(http.ResponseWriter, *http.Request) {})
	HandleFunc("/", func(http.ResponseWriter, *http.Request) {})

	_ = http.Server{}
	_ = Server{}

	_ = &http.Server{}
	_ = &Server{}

	_ = http.Client{}
	_ = Client{}

	_ = &http.Client{}
	_ = &Client{}
}

wreulicke/http-timeout

This tool isn’t part of the golangci-lint linter set; it uses Go’s internal code-checking machinery. That means you’ll have to run it separately from golangci-lint, which entails reworking your existing pipelines.

The current releases of this tool trip on the following error:

http-timeout ./...
http-timeout: internal error: package "net/http" without types was imported from "<your-module-name>"

To make the tool work, you need to rebuild it after updating its golang.org/x/tools dependency:

go get -u golang.org/x/tools
go mod tidy
goreleaser build --snapshot --rm-dist

After that the tool handles the job perfectly:

.../main.go:10:6: Do not use net/http.DefaultClient because default client has no timeout
.../main.go:13:2: Do not use net/http.Get because default client has no timeout
.../main.go:16:2: Do not use net/http.Post because default client has no timeout
.../main.go:19:2: Do not use net/http.PostForm because default client has no timeout
.../main.go:22:2: Do not use net/http.ListenAndServe because default http server has no timeout
.../main.go:25:2: Do not use net/http.Handle because default http server has no timeout
.../main.go:28:2: Do not use net/http.HandleFunc because default http server has no timeout
.../main.go:37:6: Do not use net/http.Client with no Timeout
.../main.go:40:7: Do not use net/http.Client with no Timeout
.../main.go:31:6: Do not use net/http.Server with no ReadTimeout
.../main.go:31:6: Do not use net/http.Server with no WriteTimeout
.../main.go:34:7: Do not use net/http.Server with no ReadTimeout
.../main.go:34:7: Do not use net/http.Server with no WriteTimeout

golangci-lint

You can’t simply set up a check for required http.Client struct fields with golangci-lint. But we’ll try to get close to the result above.

The configuration below detects and forbids using the default client. Don’t copy it blindly — fold the necessary bits into your existing configuration:

run:
  concurrency: 8
linters:
  disable-all: true
  enable:
    - forbidigo
    - exhaustruct
linters-settings:
  forbidigo:
    exclude-godoc-examples: true
    analyze-types: true
    forbid:
      - p: ^http\.DefaultClient.*$
        msg: 'https://blog.cloudflare.com/the-complete-guide-to-golang-net-http-timeouts/'
      - p: ^http\.Get.*$
        msg: 'https://blog.cloudflare.com/the-complete-guide-to-golang-net-http-timeouts/'
      - p: ^http\.Post.*$
        msg: 'https://blog.cloudflare.com/the-complete-guide-to-golang-net-http-timeouts/'
      - p: ^http\.PostForm.*$
        msg: 'https://blog.cloudflare.com/the-complete-guide-to-golang-net-http-timeouts/'
      - p: ^http\.ListenAndServe.*$
        msg: 'https://blog.cloudflare.com/the-complete-guide-to-golang-net-http-timeouts/'
      - p: ^http\.Handle.*$
        msg: 'https://blog.cloudflare.com/the-complete-guide-to-golang-net-http-timeouts/'
      - p: ^http\.HandleFunc.*$
        msg: 'https://blog.cloudflare.com/the-complete-guide-to-golang-net-http-timeouts/'
  exhaustruct:
    exclude:
      - ^http\.Server$
      - ^http\.Client$

Conclusion

It’s very easy to forget about the problems with http.DefaultClient if your service isn’t running under load, traffic is small, and these nuances don’t bother you.

I’ve highlighted a real problem and offered a way to deal with it. Let me know if I missed something, or if you can suggest a better approach.