Go的json.NewDecoder().Decode()似乎不遵守上下文截止时间。

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

Go json.NewDecoder().Decode() doesn't seem to respect context deadline

问题

我有一个使用了上下文截止时间的 Golang 程序。我发送了一个 HTTP 请求,并期望在读取响应体时看到一个截止时间超过的错误。

当我使用 ioutil.ReadAll 读取响应体时,读取方法会被中断,并返回适当的错误(context.DeadlineExceeded)。

然而,如果我使用 json.NewDecoder(resp.Body).Decode 读取响应体,返回的错误是 nil(而不是 context.DeadlineExceeded)。我的完整代码如下。这是 json.NewDecoder(resp.Body).Decode 的一个 bug 吗?

package main

import (
	"context"
	"encoding/json"
	"fmt"
	"io/ioutil"
	"net/http"
	"time"
)

var url string = "http://ip.jsontest.com/"

func main() {
	readDoesntFail()
	readFails()
}

type IpResponse struct {
	Ip string
}

func readDoesntFail() {
	ctx, _ := context.WithTimeout(context.Background(), time.Second*5)

	req, err := http.NewRequestWithContext(ctx, http.MethodGet, url, nil)
	if err != nil {
		panic(err)
	}
	resp, err := http.DefaultClient.Do(req)
	if err != nil {
		panic(err)
	}

	ipResponse := new(IpResponse)
	time.Sleep(time.Second * 6)
	fmt.Println("在读取响应体之前,上下文错误为:", ctx.Err())
	err = json.NewDecoder(resp.Body).Decode(ipResponse)
	if err != nil {
		panic(err)
	}
	fmt.Println("预期发生 panic,但没有发生")
}

func readFails() {
	ctx, _ := context.WithTimeout(context.Background(), time.Second*5)
	req, err := http.NewRequestWithContext(ctx, http.MethodGet, url, nil)
	if err != nil {
		panic(err)
	}
	resp, err := http.DefaultClient.Do(req)
	if err != nil {
		panic(err)
	}

	time.Sleep(time.Second * 6)
	fmt.Println("在读取响应体之前,上下文错误为:", ctx.Err())
	_, err = ioutil.ReadAll(resp.Body)
	if err != nil {
		fmt.Println("收到了预期的错误", err)
	}
}

希望对你有所帮助!

英文:

I have a Golang program with a context deadline set. I am sending an HTTP request, and expected to see a deadline exceeded error when Im reading the body.

It seems that when I read the response body with ioutil.ReadAll then that read method will get interrupted (?) and return the appropriate error (context.DeadlineExceeded).

However if I read the response body with json.NewDecoder(resp.Body).Decode then the error returned is nil (instead of context.DeadlineExceeded). My full code is below. Is this a bug in json.NewDecoder(resp.Body).Decode?

package main
import (
"context"
"encoding/json"
"fmt"
"io/ioutil"
"net/http"
"time"
)
var url string = "http://ip.jsontest.com/"
func main() {
readDoesntFail()
readFails()
}
type IpResponse struct {
Ip string
}
func readDoesntFail() {
ctx, _ := context.WithTimeout(context.Background(), time.Second*5)
req, err := http.NewRequestWithContext(ctx, http.MethodGet, url, nil)
if err != nil {
panic(err)
}
resp, err := http.DefaultClient.Do(req)
if err != nil {
panic(err)
}
ipResponse := new(IpResponse)
time.Sleep(time.Second * 6)
fmt.Println("before reading response body, context error is:", ctx.Err())
err = json.NewDecoder(resp.Body).Decode(ipResponse)
if err != nil {
panic(err)
}
fmt.Println("Expected panic but there was none")
}
func readFails() {
ctx, _ := context.WithTimeout(context.Background(), time.Second*5)
req, err := http.NewRequestWithContext(ctx, http.MethodGet, url, nil)
if err != nil {
panic(err)
}
resp, err := http.DefaultClient.Do(req)
if err != nil {
panic(err)
}
time.Sleep(time.Second * 6)
fmt.Println("before reading response body, context error is:", ctx.Err())
_, err = ioutil.ReadAll(resp.Body)
if err != nil {
fmt.Println("received expected error", err)
}
}

答案1

得分: 6

net/http 包可能会使用缓冲区来处理请求。这意味着传入的响应体可能在你读取之前被部分或全部读取和缓冲,因此过期的上下文可能无法阻止你完成读取响应体。这正是发生的情况。

让我们修改你的示例,启动一个测试的 HTTP 服务器,故意延迟响应(部分):

ts := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
    s := []byte(`{"ip":"12.34.56.78"}`)
    w.Write(s[:10])
    if f, ok := w.(http.Flusher); ok {
        f.Flush()
    }
    time.Sleep(time.Second * 6)
    w.Write(s[10:])
}))
defer ts.Close()
url = ts.URL

readDoesntFail()
readFails()

这个测试服务器发送一个类似于 ip.jsontest.com 响应的 JSON 对象。但它只发送了 10 个字节的响应体,然后刷新了缓冲区,在发送剩余部分之前故意休眠了 6 秒,"允许"客户端超时。

现在让我们看看如果调用 readDoesntFail() 会发生什么:

在读取响应体之前,上下文错误是:上下文超时
panic: Get "http://127.0.0.1:38230": 上下文超时
goroutine 1 [running]:
main.readDoesntFail()
/tmp/sandbox721114198/prog.go:46 +0x2b4
main.main()
/tmp/sandbox721114198/prog.go:28 +0x93

在你的示例中,json.Decoder.Decode() 读取的是已经缓冲的数据,所以过期的上下文在这里没有起作用。在我的示例中,json.Decoder.Decode() 尝试从连接中读取数据,因为数据尚未被缓冲(因为尚未发送),所以一旦上下文过期,从连接中进一步读取将返回一个超时错误。

英文:

The net/http package may use buffers to process requests. This means the incoming response body may be read and buffered partly or entirely before you read it, so an expiring context may not prevent you to finish reading the body. And this is exactly what happens.

Let's modify your example to fire up a test HTTP server which deliberately delays the response (partly):

ts := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
s := []byte(`{"ip":"12.34.56.78"}`)
w.Write(s[:10])
if f, ok := w.(http.Flusher); ok {
f.Flush()
}
time.Sleep(time.Second * 6)
w.Write(s[10:])
}))
defer ts.Close()
url = ts.URL
readDoesntFail()
readFails()

This test server sends a similar JSON object to that of ip.jsontest.com's response. But it only sends 10 bytes body, then flushes it, then sleeps 6 seconds on purpose before sending the rest, "allowing" the client to time out.

Now let's see what happens if we call readDoesntFail():

before reading response body, context error is: context deadline exceeded
panic: Get "http://127.0.0.1:38230": context deadline exceeded
goroutine 1 [running]:
main.readDoesntFail()
/tmp/sandbox721114198/prog.go:46 +0x2b4
main.main()
/tmp/sandbox721114198/prog.go:28 +0x93

Try it on the Go Playground.

In your example json.Decoder.Decode() reads already buffered data, so the expired context plays no role here. In my example json.Decoder.Decode() tries to read from the connection because the data isn't yet buffered (it can't be as it hasn't been sent yet), so once the context expires, further reading from the connection returns a deadline exceeded error.

huangapple
  • 本文由 发表于 2022年6月30日 00:13:09
  • 转载请务必保留本文链接:https://go.coder-hub.com/72804689.html
匿名

发表评论

匿名网友

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

确定