Golang连续发起多个请求时,会导致HTTP请求出现EOF错误。

huangapple go评论84阅读模式
英文:

Golang http request results in EOF errors when making multiple requests successively

问题

我正在尝试调试一个我写的简单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)
}

我是这样发送请求的:

// 发送HTTP请求,返回数据
func (f *firebaseRoot) SendRequest(method string, path string, body io.Reader) ([]byte, error) {
    url := f.BuildURL(path)

    // 创建请求
    req, err := http.NewRequest(method, url, body)
    if err != nil {
        return nil, err
    }

    // 发送JSON到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请求,其中一个或两个测试就会失败(有时两个都通过)。

英文:

I am trying to debug a very unusual error I am receiving for a simple REST library I wrote.

I am using the standard net/http package to make Get, Post, Put, Delete requests but my tests occasionally fail when I make multiple requests successively. My test looks like this:

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)
}

And I am making the request like this:

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

Sometimes it works, but most of the time I get 1 or 2 failures:

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

The failures happen when I make more than 1 request. If I comment out everything except for the PUT request, the tests consistently pass. Once I include a second test, such as GET, one or the other fails (sometimes both pass).

答案1

得分: 85

我经历过这个可靠的过程。您需要将Req.Close设置为true(在示例中使用的defer resp.Body.Close()语法是不够的)。像这样:

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

// 注意这个!!
req.Close = true

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

response, err = ioutil.ReadAll(resp.Body)
if err != nil {
    // 无论如何
}
英文:

I experienced this reliably. You need to set Req.Close to true (the defer on resp.Body.Close() syntax used in the examples is not enough). Like this:

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
}

答案2

得分: 41

我同意这个说法,你在单元测试中不应该访问外部服务器,为什么不直接使用内置的http.Server并提供要测试的内容呢?(实际上有一个名为httptest的包可以帮助解决这个问题)

最近我在尝试爬取网站地图时遇到了同样的问题,目前我找到了以下解决方法:

Go默认会发送带有Connection: Keep-Alive头的请求,并保持连接以便重用。我遇到的问题是服务器在响应头中返回Connection: Keep-Alive,然后立即关闭连接。

关于Go在这种情况下如何实现连接的一些背景知识(你可以在net/http/transport.go中查看完整的代码)。有两个goroutine,一个负责写入,一个负责读取(readLoopwriteLoop)。在大多数情况下,readLoop会检测到套接字关闭,并关闭连接。问题出现在当你在readLoop实际检测到关闭之前发起另一个请求时,它读取的EOF会被解释为新请求的错误,而不是在请求之前发生的关闭。

鉴于这种情况,之所以在请求之间加入睡眠时间有效,是因为它给readLoop足够的时间来检测到连接的关闭,并关闭它,这样你的新请求将会初始化一个新的连接。(而且之所以会间歇性地失败,是因为在你的请求之间有一些代码运行,并且根据goroutine的调度情况,有时EOF会在下一个请求之前被正确处理,有时不会)。而req.Close = true的解决方法之所以有效,是因为它阻止了连接的重用。

关于这种情况有一个相关的问题:https://code.google.com/p/go/issues/detail?id=4677(我创建的一个重复问题,可以可靠地重现这个问题:https://code.google.com/p/go/issues/detail?id=8122)

英文:

I agree with the assertion that you shouldn't be hitting outside servers in your unit tests, why not just use the built-in http.Server and serve up the content that you want to test. (There is actually the httptest package to help with this)

I recently ran into this same problem while trying to crawl sitemaps, and this is what I have found so far:

Go by default will send requests with the header Connection: Keep-Alive and persist connections for re-use. The problem that I ran into is that the server is responding with Connection: Keep-Alive in the response header and then immediately closing the connection.

As a little background as to how go implements connections in this case (you can look at the full code in net/http/transport.go). There are two goroutines, one responsible for writing and one responsible for reading (readLoop and writeLoop) In most circumstances readLoop will detect a close on the socket, and close down the connection. The problem here occurs when you initiate another request before the readLoop actually detects the close, and the EOF that it reads get interpreted as an error for that new request rather than a close that occurred prior to the request.

Given that this is the case the reason why sleeping in between requests works is that it gives readLoop time to detect the close on the connection before your new request and shut it down, so that your new request will initiate a new connection. (And the reason why it would intermittently fail is because there is some amount code running between your requests and depending of scheduling of goroutines, sometimes the EOF will be properly handled before your next request, sometimes not). And the req.Close = true, solution works because it prevents the connection from being re-used.

There is a ticket related to this situation: https://code.google.com/p/go/issues/detail?id=4677 (and a dupe ticket that I created that allowed me to reliably reproduce this: https://code.google.com/p/go/issues/detail?id=8122)

答案3

得分: 24

我猜测你的代码没有问题。你遇到问题的最可能原因是服务器关闭了连接。限制访问速率可能是其中一个原因。

你的测试不应该依赖于一个非常脆弱且不具备隔离性的外部服务。相反,你应该考虑在本地启动一个测试服务器。

英文:

I'm going to guess there is no problem with your code. The most likely cause of your problem is because the server is closing the connection. Rate limiting is one possible reason for this.

Your test shouldn't be relying on an external service that's very brittle and not hermetic. Instead you should think about spinning up a test server locally.

答案4

得分: 2

我的经验是当我为我的JSON API输入完全空的输入时发生了这个错误!

我应该发送{}作为空的JSON,但我发送了 ,所以发生了这个错误。

英文:

My experience with this error was when I entered absolutely empty input for my JSON API!

I should send {} as empty JSON, but I sent so this error happened

答案5

得分: 1

我正在开发一个图片下载应用程序时遇到了这个问题。尝试了request.Close=true但没有起作用。60%的请求导致了一个EOF错误。

我以为可能是图片服务器的问题,而不是我的代码。但是PHP代码运行良好。

然后我使用了以下代码来发送请求:

var client = &http.Client{
	Transport: &http.Transport{},
}
client.Do(request)

而不是使用

http.DefaultClient.Do(request)

问题解决了。不确定为什么,我猜可能与RoundTripper有关。

英文:

I was developing an image download app when this problem occurs.
Tried request.Close=true but not work.
60% requests resulted in a EOF error.

I thought maybe it is an image server problem, not my code.
But php code works fine.

Then I use

var client = &http.Client{
	Transport: &http.Transport{},
}
client.Do(request)

to make request, instead of

http.DefaultClient.Do(request)

problem gone.
Not sure why,I guess something with RoundTripper

答案6

得分: 0

我在发送一个无效的请求体到一个GET请求时遇到了这个问题。

我可以通过发送类似下面的请求来复现:

var requestBody interface{}
requestData, _ := json.Marshal(requestBody)
payload = strings.NewReader(string(requestData))

req, err := http.NewRequest("GET", url, payload)
...
英文:

I encountered this issue while sending an invalid body to a GET request

I could reproduce by making a request similar to below:

var requestBody interface{}
requestData, _ := json.Marshal(requestBody)
payload = strings.NewReader(string(requestData))

req, err := http.NewRequest("GET", url, payload)
...

答案7

得分: 0

我遇到了同样的问题,经过多个小时的调查,发现CDN关闭了连接,导致我们这边出现了EOF错误。而不希望关闭的原因是我设置的请求中的User-Agent。我使用了一个随机选择的浏览器User-Agent,描述了一个8年前的Chrome版本。可能请求被视为不受信任的请求。更新User-Agent后,问题得到解决。

英文:

I faced the same issue, and after investigating for many hours, it turned out that the CDN closed the connection and caused an EOF error on our side. And the reason of the unwanted close was the User-Agent what I set to the request. I used a randomly picked browser User-Agent, which described an 8-years old version of the Chrome. Probably the request was treated as an untrusted one. After updating the User-Agent the issue has been solved.

huangapple
  • 本文由 发表于 2013年7月18日 12:22:09
  • 转载请务必保留本文链接:https://go.coder-hub.com/17714494.html
匿名

发表评论

匿名网友

:?: :razz: :sad: :evil: :!: :smile: :oops: :grin: :eek: :shock: :???: :cool: :lol: :mad: :twisted: :roll: :wink: :idea: :arrow: :neutral: :cry: :mrgreen:

确定