網頁

2022/6/1

Golang http Client context請求失敗

Go http.Client發送附帶context的請求時若得到回應前發生context超時、context取消或超過client時間限制會導致請求失敗。


範例環境:

  • Go 1.18


範例

下面在GET()函式中呼叫http.NewRequestWithContext()建立帶context及指定url的GET請求物件http.Request,然後建立http.Client設定回應限時3秒,最後將請求物件傳入http.Client.Do()發出請求。

main.go

package main

import (
    "context"
    "fmt"
    "io"
    "log"
    "net/http"
    "time"
)

func main() {
    ...
}

func Get(ctx context.Context, url string) (string, error) {
    req, err := http.NewRequestWithContext(
        ctx,
        http.MethodGet,
        url,
        nil)
    if err != nil {
        log.Printf("create request error, err=%v", err)
        return "", err
    }

    client := &http.Client{
        Timeout: time.Second * 3,
    }
    
    resp, err := client.Do(req) // 發送請求
    if err != nil {
        log.Printf("client send request error, err=%v", err)
        return "", err
    }
    defer resp.Body.Close()

    b, err := io.ReadAll(resp.Body)
    if err != nil {
        log.Printf("read request body error, err=%v", err)
        return "", err
    }
    return string(b), nil
}


測試

在測試程式main_test.go中測試下面幾種狀況:

  • context deadline exceeded - context超過期限
  • context canceled - context已取消
  • context deadline exceeded (Client.Timeout exceeded while awaiting headers) - Client等待headers超過期限


context deadline exceeded

TestGet_ContextDeadlineExceeded()中使用context.WithTimeout(ctx, time.Second*1)設定context期限1秒,並在mock servertime.Sleep(time.Second * 2)延遲2秒才回應。由於context會先到期,所以http.Client.Do()回傳錯誤context deadline exceeded

main_test.go

package main

import (
    "context"
    "net/http"
    "net/http/httptest"
    "testing"
    "time"
)

...
func TestGet_ContextDeadlineExceeded(t *testing.T) {
    ts := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
        w.WriteHeader(http.StatusOK)
        time.Sleep(time.Second * 2)
        w.Write([]byte("hello"))
    }))
    defer ts.Close()

    ctx := context.Background()
    ctx, cancel := context.WithTimeout(ctx, time.Second*1)
    defer cancel()

    _, err := Get(ctx, ts.URL)
    if err == nil {
        t.Error("unexpected success")
    }
}
...

執行測試如下:

$ go test -run TestGet_ContextDeadlineExceeded -v
=== RUN   TestGet_ContextDeadlineExceeded
2022/06/01 23:53:21 client send request error, err=Get "http://127.0.0.1:53771": context deadline exceeded
--- PASS: TestGet_ContextDeadlineExceeded (2.01s)
PASS
ok      abc.com/demo    2.394s


context canceled

TestGet_ContextCanceled()中使用context.WithCancel()設定context取得context.CancelFunc並馬上執行(取消)。在mock server用time.Sleep(time.Second * 2)延遲2秒才回應。由於context先被取消,所以http.Client.Do()回傳錯誤context canceled

main_test.go

package main

import (
    "context"
    "net/http"
    "net/http/httptest"
    "testing"
    "time"
)

...
func TestGet_ContextCanceled(t *testing.T) {
    ts := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
        w.WriteHeader(http.StatusOK)
        time.Sleep(time.Second * 2)
        w.Write([]byte("hello"))
    }))
    defer ts.Close()

    ctx := context.Background()
    ctx, cancel := context.WithCancel(ctx)
    cancel()

    _, err := Get(ctx, ts.URL)
    if err == nil {
        t.Error("unexpected success")
    }
}
...

執行測試如下:

$ go test -run TestGet_ContextCanceled -v
=== RUN   TestGet_ContextCanceled
2022/06/01 23:59:17 client send request error, err=Get "http://127.0.0.1:53802": context canceled
--- PASS: TestGet_ContextCanceled (0.00s)
PASS
ok      abc.com/demo    0.393s


context deadline exceeded (Client.Timeout exceeded while awaiting headers)

TestGet_ClientTimeout()中在mock server用time.Sleep(time.Second * 5)延遲5秒才回應。由於受測的Get()Client.Timeout設為限時3秒回應,所以http.Client.Do()回傳錯誤context deadline exceeded (Client.Timeout exceeded while awaiting headers)

main_test.go

package main

import (
    "context"
    "net/http"
    "net/http/httptest"
    "testing"
    "time"
)

...
func TestGet_ClientTimeout(t *testing.T) {
    ts := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
        w.WriteHeader(http.StatusOK)
        time.Sleep(time.Second * 5)
        w.Write([]byte("hello"))
    }))
    defer ts.Close()

    _, err := Get(context.Background(), ts.URL)
    if err == nil {
        t.Error("unexpected success")
    }
}

執行測試如下:

$ go test -run TestGet_ClientTimeout -v
=== RUN   TestGet_ClientTimeout
2022/06/02 00:06:07 client send request error, err=Get "http://127.0.0.1:53844": context deadline exceeded (Client.Timeout exceeded while awaiting headers)
--- PASS: TestGet_ClientTimeout (5.00s)
PASS
ok      abc.com/demo    5.325s

github



沒有留言:

張貼留言