Skip to content

Commit 885ca92

Browse files
committed
fix: Timeout
1 parent ae09f32 commit 885ca92

17 files changed

+429
-275
lines changed

.github/workflows/go.yml

+3-4
Original file line numberDiff line numberDiff line change
@@ -23,13 +23,12 @@ jobs:
2323

2424
- name: Build
2525
run: go build -v ./...
26-
2726
- name: Test
28-
run: go test -v ./...
29-
27+
run: go test -v -race ./...
28+
- name: Benchmark
29+
run: go test -race -run=^$ -bench=. -benchmem ./...
3030
- name: Coverage
3131
run: go test -coverprofile=coverage.txt
32-
3332
- name: Upload coverage reports to Codecov
3433
uses: codecov/codecov-action@v5
3534
with:

api.go

+7-7
Original file line numberDiff line numberDiff line change
@@ -8,36 +8,36 @@ import (
88
"strings"
99
)
1010

11-
var s = New()
11+
var session = New()
1212

1313
// Get send get request
1414
func Get(url string) (*http.Response, error) {
15-
return s.Do(context.Background(), MethodGet, URL(url))
15+
return session.Do(context.Background(), MethodGet, URL(url))
1616
}
1717

1818
// Post send post request
1919
func Post(url string, contentType string, body io.Reader) (*http.Response, error) {
20-
return s.Do(context.TODO(), MethodPost, URL(url), Header("Content-Type", contentType), Body(body))
20+
return session.Do(context.TODO(), MethodPost, URL(url), Header("Content-Type", contentType), Body(body))
2121
}
2222

2323
// PUT send put request
2424
func PUT(url, contentType string, body io.Reader) (*http.Response, error) {
25-
return s.Do(context.TODO(), Method("PUT"), URL(url), Header("Content-Type", contentType), Body(body))
25+
return session.Do(context.TODO(), Method("PUT"), URL(url), Header("Content-Type", contentType), Body(body))
2626
}
2727

2828
// Delete send delete request
2929
func Delete(url, contentType string, body io.Reader) (*http.Response, error) {
30-
return s.Do(context.TODO(), Method("DELETE"), URL(url), Header("Content-Type", contentType), Body(body))
30+
return session.Do(context.TODO(), Method("DELETE"), URL(url), Header("Content-Type", contentType), Body(body))
3131
}
3232

3333
// Head send post request
3434
func Head(url string) (resp *http.Response, err error) {
35-
return s.Do(context.Background(), Method("HEAD"), URL(url))
35+
return session.Do(context.Background(), Method("HEAD"), URL(url))
3636
}
3737

3838
// PostForm send post request, content-type = application/x-www-form-urlencoded
3939
func PostForm(url string, data url.Values) (*http.Response, error) {
40-
return s.Do(context.TODO(), MethodPost, URL(url), Header("Content-Type", "application/x-www-form-urlencoded"),
40+
return session.Do(context.TODO(), MethodPost, URL(url), Header("Content-Type", "application/x-www-form-urlencoded"),
4141
Body(strings.NewReader(data.Encode())),
4242
)
4343
}

requests_test.go

+34-14
Original file line numberDiff line numberDiff line change
@@ -4,32 +4,38 @@ import (
44
"context"
55
"fmt"
66
"io"
7-
"log"
87
"net"
98
"net/http"
109
"net/http/httptest"
1110
"net/url"
1211
"os"
1312
"path"
13+
"strings"
1414
"sync/atomic"
1515
"testing"
1616
"time"
1717
)
1818

19-
func Test_Basic(t *testing.T) {
20-
resp, err := Get("http://httpbin.org/get")
21-
t.Logf("%#v, %v", resp, err)
22-
//resp, _ = Post("http://httpbin.org/post", "application/json", strings.NewReader(`{"a": "b"}`))
23-
//t.Log(resp.Text())
19+
var s *Server
20+
21+
func TestMain(m *testing.M) {
22+
mux := NewServeMux()
23+
mux.HandleFunc("/", func(w http.ResponseWriter, r *http.Request) {
24+
w.Write([]byte("hello world"))
25+
})
26+
s = NewServer(context.Background(), mux, URL("http://127.0.0.1:65534"))
27+
28+
go s.ListenAndServe()
29+
defer s.Shutdown(context.Background())
30+
os.Exit(m.Run())
2431
}
2532

2633
func Test_ProxyGet(t *testing.T) {
27-
t.Log("Testing get request")
2834
sess := New(
2935
Header("a", "b"),
3036
Cookie(http.Cookie{Name: "username", Value: "golang"}),
3137
BasicAuth("user", "123456"),
32-
Timeout(3*time.Second),
38+
Timeout(5*time.Second),
3339
//Hosts(map[string][]string{"127.0.0.1:8080": {"192.168.1.1:80"}, "4.org:80": {"httpbin.org:80"}}),
3440
//Proxy("http://127.0.0.1:8080"),
3541
)
@@ -103,14 +109,14 @@ func Test_FormPost(t *testing.T) {
103109
//TraceLv(9),
104110
)
105111
if err != nil {
106-
log.Fatal(err)
112+
t.Fatal(err)
107113
return
108114
}
109115
t.Log(resp.StatusCode, err, resp.Response.ContentLength, resp.Request.ContentLength)
110116

111117
}
112118

113-
func Test_Race(t *testing.T) {
119+
func Test_DoRequestRace(t *testing.T) {
114120
opts := Options{}
115121
ctx := context.Background()
116122
t.Logf("%#v", opts)
@@ -176,7 +182,7 @@ func TestResponse_Download(t *testing.T) {
176182
sum += cnt
177183
return err
178184
})
179-
resp, err := sess.DoRequest(context.Background(), Setup(Redirect))
185+
resp, err := sess.DoRequest(context.Background(), Trace())
180186
if err != nil {
181187
t.Logf("resp=%d, err=%s", resp.Content, err)
182188
return
@@ -197,11 +203,25 @@ func TestRequestWithTimeout(t *testing.T) {
197203
}))
198204
defer server.Close()
199205

206+
setup := func(name string) func(next http.RoundTripper) http.RoundTripper {
207+
return func(next http.RoundTripper) http.RoundTripper {
208+
return RoundTripperFunc(func(r *http.Request) (*http.Response, error) {
209+
t.Logf("timeout %s test prev", name)
210+
defer t.Logf("timeout %s test next", name)
211+
return next.RoundTrip(r)
212+
})
213+
}
214+
}
215+
200216
// 测试超时情况
201-
sess := New(Timeout(10 * time.Millisecond))
202-
_, err := sess.DoRequest(context.Background(), URL(server.URL))
217+
sess := New(Timeout(10*time.Millisecond), Logf(LogS), Setup(setup("session0"), setup("session1")))
218+
_, err := sess.DoRequest(context.Background(), URL(server.URL), Setup(setup("request0-0"), setup("request0-1")))
219+
t.Logf("timeout err=%v", err)
203220
if err == nil {
204-
t.Skip("期望超时错误,但没有发生")
221+
t.Error("期望发生超时错误,但没有")
222+
}
223+
if !strings.Contains(err.Error(), "Client.Timeout exceeded") {
224+
t.Error("发生错误,但不是超时")
205225
}
206226

207227
// 测试非超时情况

response.go

+31-38
Original file line numberDiff line numberDiff line change
@@ -8,42 +8,61 @@ import (
88
"time"
99
)
1010

11-
// Response wrap `http.Response` struct.
11+
// Response 包装了 http.Response 结构体,提供了额外的功能:
12+
// 1. 记录请求开始时间和耗时
13+
// 2. 缓存响应内容
14+
// 3. 错误处理
15+
// 4. 统计信息收集
1216
type Response struct {
13-
*http.Request
14-
*http.Response
15-
StartAt time.Time
16-
Cost time.Duration
17-
Content *bytes.Buffer
18-
Err error
17+
*http.Request // 原始 HTTP 请求
18+
*http.Response // 原始 HTTP 响应
19+
StartAt time.Time // 请求开始时间
20+
Cost time.Duration // 请求耗时
21+
Content *bytes.Buffer // 响应内容缓存
22+
Err error // 请求过程中的错误
1923
}
2024

25+
// newResponse 创建一个新的 Response 实例
26+
// 参数 r 是原始的 HTTP 请求
2127
func newResponse(r *http.Request) *Response {
22-
return &Response{Request: r, StartAt: time.Now(), Response: &http.Response{}, Content: &bytes.Buffer{}}
28+
return &Response{
29+
Request: r,
30+
StartAt: time.Now(),
31+
Response: &http.Response{},
32+
Content: &bytes.Buffer{},
33+
}
2334
}
2435

25-
// String implement fmt.Stringer interface.
36+
// String 实现 fmt.Stringer 接口
37+
// 返回响应内容的字符串形式
2638
func (resp *Response) String() string {
2739
return resp.Content.String()
2840
}
2941

30-
// Error implement error interface.
42+
// Error 实现 error 接口
43+
// 返回请求过程中的错误信息
3144
func (resp *Response) Error() string {
3245
if resp.Err == nil {
3346
return ""
3447
}
3548
return resp.Err.Error()
3649
}
3750

38-
// Stat stat
51+
// Stat 返回请求的统计信息
52+
// 包括请求/响应的详细信息、耗时等
3953
func (resp *Response) Stat() *Stat {
4054
return responseLoad(resp)
4155
}
4256

43-
// streamRead xx
57+
// streamRead 按行读取数据流
58+
// reader: 输入的数据流
59+
// fn: 处理每一行数据的回调函数,参数为行号和行内容
60+
// 返回值:读取的总字节数和可能的错误
4461
func streamRead(reader io.Reader, fn func(int64, []byte) error) (int64, error) {
62+
// 创建一个 1MB 缓冲的读取器
4563
i, cnt, r := int64(0), int64(0), bufio.NewReaderSize(reader, 1024*1024)
4664
for {
65+
// 读取直到遇到换行符
4766
raw, err1 := r.ReadBytes(10) // ascii('\n') = 10
4867
if err1 != nil && err1 != io.EOF {
4968
return cnt, err1
@@ -52,32 +71,6 @@ func streamRead(reader io.Reader, fn func(int64, []byte) error) (int64, error) {
5271
i, cnt = i+1, cnt+int64(len(raw))
5372
if err2 := fn(i, raw); err1 == io.EOF || err2 != nil {
5473
return cnt, err2
55-
5674
}
5775
}
5876
}
59-
60-
// streamRoundTrip 创建一个流式处理的RoundTripper中间件
61-
// 参数 f 是流处理回调函数,接收两个参数:
62-
// - i int64: 当前处理的数据块序号(从1开始)
63-
// - raw []byte: 原始数据块内容(按换行符分割)
64-
//
65-
// 返回值是可用于HTTP客户端中间件链的RoundTripper
66-
// 适用场景:大文件下载、实时事件流处理等需要边接收边处理的场景
67-
// 注意:与普通RoundTripper不同,此方法会流式处理响应体而不是缓存全部内容
68-
func streamRoundTrip(fn func(i int64, raw []byte) error) func(http.RoundTripper) http.RoundTripper {
69-
return func(next http.RoundTripper) http.RoundTripper {
70-
return RoundTripperFunc(func(r *http.Request) (*http.Response, error) {
71-
resp := newResponse(r)
72-
if resp.Response, resp.Err = next.RoundTrip(r); resp.Err != nil {
73-
return resp.Response, resp.Err
74-
}
75-
76-
if resp.Response.ContentLength, resp.Err = streamRead(resp.Response.Body, fn); resp.Err != nil {
77-
return resp.Response, resp.Err
78-
}
79-
resp.Response.Body = io.NopCloser(bytes.NewReader([]byte("[stream]")))
80-
return resp.Response, resp.Err
81-
})
82-
}
83-
}

response_test.go

-69
Original file line numberDiff line numberDiff line change
@@ -3,8 +3,6 @@ package requests
33
import (
44
"bytes"
55
"errors"
6-
"fmt"
7-
"io"
86
"net/http"
97
"strings"
108
"testing"
@@ -119,73 +117,6 @@ func TestStreamReadError(t *testing.T) {
119117
}
120118
}
121119

122-
// TestStreamRoundTrip 测试streamRoundTrip中间件
123-
func TestStreamRoundTrip(t *testing.T) {
124-
// 创建一个模拟的响应体
125-
responseBody := "line1\nline2\nline3"
126-
expectedLines := []string{"line1", "line2", "line3"}
127-
128-
// 创建一个模拟的RoundTripper
129-
mockTransport := RoundTripperFunc(func(req *http.Request) (*http.Response, error) {
130-
return &http.Response{
131-
StatusCode: 200,
132-
Body: io.NopCloser(strings.NewReader(responseBody)),
133-
}, nil
134-
})
135-
136-
// 创建请求
137-
req, _ := http.NewRequest("GET", "http://example.com", nil)
138-
139-
// 收集处理的行
140-
var lines []string
141-
middleware := streamRoundTrip(func(_ int64, data []byte) error {
142-
lines = append(lines, string(bytes.TrimRight(data, "\n")))
143-
return nil
144-
})
145-
146-
// 执行请求
147-
resp, err := middleware(mockTransport).RoundTrip(req)
148-
if err != nil {
149-
t.Fatalf("RoundTrip failed: %v", err)
150-
}
151-
152-
// 验证响应
153-
if resp.StatusCode != 200 {
154-
t.Errorf("Expected status 200, got %d", resp.StatusCode)
155-
}
156-
157-
// 验证处理的行
158-
if len(lines) != len(expectedLines) {
159-
t.Errorf("Expected %d lines, got %d", len(expectedLines), len(lines))
160-
}
161-
for i, line := range expectedLines {
162-
if lines[i] != line {
163-
t.Errorf("Line %d: expected '%s', got '%s'", i, line, lines[i])
164-
}
165-
}
166-
}
167-
168-
// BenchmarkStreamRead 性能测试
169-
func BenchmarkStreamRead(b *testing.B) {
170-
// 准备测试数据
171-
var testData strings.Builder
172-
for i := 0; i < 1000; i++ {
173-
fmt.Fprintf(&testData, "Line %d\n", i)
174-
}
175-
data := testData.String()
176-
177-
b.ResetTimer()
178-
for i := 0; i < b.N; i++ {
179-
reader := strings.NewReader(data)
180-
_, err := streamRead(reader, func(_ int64, _ []byte) error {
181-
return nil
182-
})
183-
if err != nil {
184-
b.Fatal(err)
185-
}
186-
}
187-
}
188-
189120
// 用于测试的错误读取器
190121
type errorReader struct {
191122
err error

0 commit comments

Comments
 (0)