http.DefaultClient

“1024 плети тому, кто использует http.DefaultClient в своём коде” — такая “шутка” звучит на наших стендапах. Проблема заключается в отсутствии таймаутов в настройках по умолчанию. На эту тему есть отличная статья в блоге Cloudflare1.

Мы же попробуем не допустить использование http.DefaultClient в нашем коде. Для этого есть как минимум два инструмента:

  • wreulicke/http-timeout2
  • golangci-lint

Для экспериментов нам потребуется пример кода:

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

Инструмент не входит в состав линтеров golangci-lint и использует внутренние механизмы проверки кода на Go. Это значит, что запускать его потребуется отдельно от golangci-lint, а это влечёт за собой доработку существующих пайплайнов.

Текущие релизы этого инструмента спотыкаются на ошибке:

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

Чтобы инструмент заработал, его нужно пересобрать, обновив зависимость от golang.org/x/tools:

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

После чего инструмент со своей задачей справляется отлично:

.../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

Просто так взять и настроить проверку обязательных для заполнения полей структуры http.Client с помощью golangci-lint сделать не получится. Но мы постараемся приблизиться к результату, описанному выше.

Конфигурация, приведённая ниже, позволяет находить и запрещать использование клиента по умолчанию. Не копируйте её бездумно, добавьте необходимое в вашу уже существующую конфигурацию:

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$

Вывод

Очень легко забыть о проблемах использования http.DefaultClient, если ваше решение работает не под нагрузкой, траффик маленький и вас эти нюансы не беспокоят.

Я подсветил реальную проблему и предложил способ её решения. Дайте знать, если я что-то упустил или можете предложить более подходящее решение.