2013-07-18 678 views
14

我正在尝试调试一个非常不寻常的错误,我正在为一个简单的REST库I wrote收到。当连续发出多个请求时,Golang http请求会导致EOF错误

我使用标准的net/http软件包来获取,发布,放置,删除请求,但是当我连续发出多个请求时,我的测试偶尔会失败。我的测试是这样的:

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

+0

请出示完整的代码。 – Volker

+0

从'错误:发布https://go-firebase-test.firebaseio.com/.json:EOF'这一行看起来应该有'.json'之前的文件名。如果'.json'不是web-root中的有效文件名,它将立即返回一个'EOF'。检查创建URL字符串的函数。我认为这将是问题 – Intermernet

+0

感谢您的评论,我会给你一个镜头。 – chourobin

回答

12

我会想有你的代码没有问题。您的问题最可能的原因是因为服务器正在关闭连接。限速是造成这种情况的一个可能的原因。

你的测试不应该依赖于非常脆弱而不是密封的外部服务。相反,您应该考虑在本地启动测试服务器。

+0

谢谢,我跑了测试之间的睡眠之间,它看起来像它的工作再次。限速是可能的。 – chourobin

+1

这个答案是错误的(并且由于错误的原因而宣扬)。 @Alex Davies'的答案是正确的 – mwag

37

我经历过这个可靠的。您需要将Req.Close设置为true(对示例中使用的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 
} 
+1

只是来自Go docs:request.Close [a bool]的一个FYI指示在发送请求(针对客户端)后,在回复此请求(针对服务器)还是**之后是否关闭连接* *。 – jsherer

+0

这正是我所需要的。谢谢!我的用例是通过CLI应用程序创建GitHub Oauth令牌,其中第二个请求必须与OTP授权码一起发布。如果没有'req.Close',我得到一个“http:无法在断开的连接上写HTTP请求”的错误,并且它的一切都按预期工作。 –

+0

这个bug应该在Go 1.6中修复,请参阅https://go-review.googlesource.com/#/c/3210/ – petrkotek

18

我与你不应该打在你的单元测试之外的服务器断言同意,为什么不直接使用内置的http.Server和服务于你要测试的内容。 (实际上还有就是httptest包,以帮助这个)

我最近遇到了同样的问题,而试图抓取网站地图,这是我迄今为止发现:

转到默认会发送请求与标头Connection: Keep-Alive和坚持连接重新使用。我碰到的问题是服务器在响应头中响应Connection: Keep-Alive,然后立即关闭连接。

作为在这种情况下如何实现连接的小背景(您可以查看net/http/transport.go中的完整代码)。有两个goroutines,一个负责写入,另一个负责读取(readLoopwriteLoop)在大多数情况下,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

+0

我正在使用httptest软件包,但仍然在测试中遇到此问题。然后,我滚动了足够远的输出,发现运行时恐慌导致我的测试服务器无法关闭连接。 – Omn

+0

这很有道理。每当我连续发出第二个请求时,我都会收到EOF错误。它让我疯狂,因为如果我在Python CLI中执行了相同的一系列请求,一切都已经完成,所以我知道这是Go的具体情况。我只是在想,它是目标服务器对每一个第二次请求都做了什么事情,这个请求专门打乱了Go的'http'实现。 – jeteon