Результаты запроса Golang http приводят к ошибкам EOF при последовательном выполнении нескольких запросов

Я пытаюсь отладить очень необычную ошибку, которую я получаю для простой библиотеки REST, которую я написал.

Я использую стандартный net/http-пакет, чтобы делать запросы Get, Post, Put, Delete, но мои тесты иногда терпят неудачу, когда я делаю несколько запросов подряд. Мой тест выглядит следующим образом:

func TestGetObject(t *testing.T) {
    firebaseRoot := New(firebase_url)
    body, err := firebaseRoot.Get("1")
    if err != nil {
        t.Errorf("Error: %s", err)
    }
    t.Logf("%q", body)
}  

func TestPushObject(t *testing.T) {
    firebaseRoot := New(firebase_url)
    msg := Message{"testing", "1..2..3"}
    body, err := firebaseRoot.Push("/", msg)
    if err != nil {
        t.Errorf("Error: %s", err)
    }
    t.Logf("%q", body)
}

И я делаю запрос следующим образом:

// Send HTTP Request, return data
func (f *firebaseRoot) SendRequest(method string, path string, body io.Reader) ([]byte, error) {
url := f.BuildURL(path)

// create a request
req, err := http.NewRequest(method, url, body)
if err != nil {
    return nil, err
}

// send JSON to firebase
resp, err := http.DefaultClient.Do(req)
if err != nil {
    return nil, err
}

if resp.StatusCode != http.StatusOK {
    return nil, fmt.Errorf("Bad HTTP Response: %v", resp.Status)
}

defer resp.Body.Close()
b, err := ioutil.ReadAll(resp.Body)
if err != nil {
    return nil, err
}

return b, nil
} 

Иногда это работает, но большую часть времени я получаю 1 или 2 отказа:

--- FAIL: TestGetObject (0.00 seconds)
firebase_test.go:53: Error: Get https://go-firebase-test.firebaseio.com/1.json: EOF
firebase_test.go:55: ""

--- FAIL: TestPushObject (0.00 seconds)
firebase_test.go:63: Error: Post https://go-firebase-test.firebaseio.com/.json: EOF
firebase_test.go:65: ""
FAIL
exit status 1
FAIL    github.com/chourobin/go.firebase    3.422s

Ошибки случаются, когда я делаю более одного запроса. Если я прокомментирую все, кроме запроса PUT, тесты будут проходить последовательно. После включения второго теста, такого как GET, один или другой сбой (иногда оба проходят).

Любая помощь ценится, и спасибо!

Ссылка на источник: http://github.com/chourobin/go.firebase

Ответы

Ответ 1

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

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

Ответ 2

Я испытал это надежно. Вам нужно установить Req.Close в true (синтаксис defer on resp.Body.Close(), используемый в примерах, недостаточно). Вот так:

client := &http.Client{}
req, err := http.NewRequest(method, url, httpBody)

// NOTE this !!
req.Close = true

req.Header.Set("Content-Type", "application/json")
req.SetBasicAuth("user", "pass")
resp, err := client.Do(req)
if err != nil {
    // whatever
}
defer resp.Body.Close()

response, err = ioutil.ReadAll(resp.Body)
if err != nil {
    // Whatever
}

Ответ 3

Я согласен с утверждением, что вы не должны бить внешние серверы в своих модульных тестах, почему бы просто не использовать встроенный http.Server и не обслуживать контент, который вы хотите проверить. (Существует действительно пакет httptest, чтобы помочь с этим)

Недавно я столкнулся с этой проблемой при попытке обхода файлов Sitemap, и это то, что я нашел до сих пор:

Go по умолчанию отправляет запросы с заголовком Connection: Keep-Alive и сохраняет соединения для повторного использования. Проблема, с которой я столкнулся, заключается в том, что сервер отвечает Connection: Keep-Alive в заголовке ответа, а затем сразу же закрывает соединение.

В качестве небольшого справочника о том, как go реализует соединения в этом случае (вы можете посмотреть полный код в net/http/transport.go). Есть два goroutines, один отвечает за запись и один отвечает за чтение (readLoop и writeLoop). В большинстве случаев readLoop обнаружит закрытие сокета и закроет соединение. Проблема возникает, когда вы инициируете другой запрос до того, как readLoop фактически обнаруживает закрытие, и EOF, который он читает, интерпретируется как ошибка для этого нового запроса, а не закрытие, которое произошло до запроса.

Учитывая, что это так, причина, по которой спать между запросами работает, заключается в том, что она дает readLoop время для обнаружения закрытия соединения перед вашим новым запросом и его закрытия, так что ваш новый запрос инициирует новое соединение. (И причина, по которой она прерывалась с ошибкой, заключается в том, что между вашими запросами и некоторым количеством планирования goroutines существует некоторый код количества, иногда EOF будет надлежащим образом обрабатываться до вашего следующего запроса, иногда нет). И решение req.Close = true работает, потому что оно предотвращает повторное использование соединения.

Есть билет, связанный с этой ситуацией: https://code.google.com/p/go/issues/detail?id=4677 (и дублированный билет, который я создал, позволил мне достоверно воспроизвести это: https://code.google.com/p/go/issues/detail?id=8122)