Skip to content

Commit 58af708

Browse files
committed
优化:代理探测换备用探测点并精简失败提示
注意:本次改动不由账号锁死修复引入,是工作区中已存在的他处改动, 单独成一条提交以便按需回退。 - 备用探测点由 httpbin.org 换成 api64.ipify.org。 - 全部探测点失败时不再把最后一条原始错误直接抛给前端,改为聚合成一句 精简提示(每个探测点一个短名 + 归一化后的失败原因,超长截断),完整的 逐个错误保留在 Unwrap 链上供 errors.Is/As 使用。
1 parent d4d7f14 commit 58af708

2 files changed

Lines changed: 136 additions & 28 deletions

File tree

‎backend/internal/repository/proxy_probe_service.go‎

Lines changed: 67 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -3,10 +3,13 @@ package repository
33
import (
44
"context"
55
"encoding/json"
6+
"errors"
67
"fmt"
78
"io"
89
"log"
10+
"net"
911
"net/http"
12+
"net/url"
1013
"strings"
1114
"time"
1215

@@ -48,12 +51,16 @@ const (
4851
// 某些 AI API 专用代理只允许访问特定域名,因此需要多个备选
4952
var probeURLs = []struct {
5053
url string
51-
parser string // "ip-api" or "httpbin"
54+
name string // 聚合错误信息里的短名,避免把完整 URL 拼进提示
55+
parser string // "ip-api" or "ipify"
5256
}{
53-
{"http://ip-api.com/json/?lang=zh-CN", "ip-api"},
54-
{"http://httpbin.org/ip", "httpbin"},
57+
{"http://ip-api.com/json/?lang=zh-CN", "ip-api", "ip-api"},
58+
{"http://api64.ipify.org?format=json", "ipify", "ipify"},
5559
}
5660

61+
// maxProbeReasonLen 单个探测点失败原因在聚合信息里的最大长度(按 rune 计)
62+
const maxProbeReasonLen = 60
63+
5764
type proxyProbeService struct {
5865
insecureSkipVerify bool
5966
allowPrivateHosts bool
@@ -73,16 +80,62 @@ func (s *proxyProbeService) ProbeProxy(ctx context.Context, proxyURL string) (*s
7380
return nil, 0, fmt.Errorf("failed to create proxy client: %w", err)
7481
}
7582

76-
var lastErr error
83+
reasons := make([]string, 0, len(probeURLs))
84+
errs := make([]error, 0, len(probeURLs))
7785
for _, probe := range probeURLs {
7886
exitInfo, latencyMs, err := s.probeWithURL(ctx, client, probe.url, probe.parser)
7987
if err == nil {
8088
return exitInfo, latencyMs, nil
8189
}
82-
lastErr = err
90+
reasons = append(reasons, probe.name+": "+summarizeProbeError(err))
91+
errs = append(errs, fmt.Errorf("%s: %w", probe.name, err))
92+
}
93+
94+
return nil, 0, &probeFailureError{
95+
summary: fmt.Sprintf("all probe URLs failed (%s)", strings.Join(reasons, "; ")),
96+
errs: errs,
8397
}
98+
}
99+
100+
// probeFailureError 对外只暴露一条精简的聚合提示,完整的逐个探测错误保留在
101+
// Unwrap 链上供 errors.Is/As 使用,避免把每个探测点的原始报文都堆到前端弹窗里。
102+
type probeFailureError struct {
103+
summary string
104+
errs []error
105+
}
106+
107+
func (e *probeFailureError) Error() string { return e.summary }
108+
109+
func (e *probeFailureError) Unwrap() []error { return e.errs }
84110

85-
return nil, 0, fmt.Errorf("all probe URLs failed, last error: %w", lastErr)
111+
// summarizeProbeError 把单个探测点的失败原因压成一句短语:
112+
// 常见网络故障归一成固定词,其余去掉 net/http 附带的完整 URL 后截断。
113+
func summarizeProbeError(err error) string {
114+
if err == nil {
115+
return "unknown"
116+
}
117+
if errors.Is(err, context.Canceled) {
118+
return "canceled"
119+
}
120+
var netErr net.Error
121+
if errors.Is(err, context.DeadlineExceeded) || (errors.As(err, &netErr) && netErr.Timeout()) {
122+
return "timeout"
123+
}
124+
125+
msg := err.Error()
126+
// url.Error 会把完整探测地址拼进消息(如 `Get "http://ip-api.com/...": xxx`),剥掉它
127+
var urlErr *url.Error
128+
if errors.As(err, &urlErr) {
129+
msg = strings.Replace(msg, fmt.Sprintf("%s %q: ", urlErr.Op, urlErr.URL), "", 1)
130+
}
131+
msg = strings.Join(strings.Fields(msg), " ")
132+
if msg == "" {
133+
return "unknown"
134+
}
135+
if runes := []rune(msg); len(runes) > maxProbeReasonLen {
136+
msg = strings.TrimSpace(string(runes[:maxProbeReasonLen])) + "…"
137+
}
138+
return msg
86139
}
87140

88141
func (s *proxyProbeService) probeWithURL(ctx context.Context, client *http.Client, url string, parser string) (*service.ProxyExitInfo, int64, error) {
@@ -119,8 +172,8 @@ func (s *proxyProbeService) probeWithURL(ctx context.Context, client *http.Clien
119172
switch parser {
120173
case "ip-api":
121174
return s.parseIPAPI(body, latencyMs)
122-
case "httpbin":
123-
return s.parseHTTPBin(body, latencyMs)
175+
case "ipify":
176+
return s.parseIPify(body, latencyMs)
124177
default:
125178
return nil, latencyMs, fmt.Errorf("unknown parser: %s", parser)
126179
}
@@ -165,18 +218,17 @@ func (s *proxyProbeService) parseIPAPI(body []byte, latencyMs int64) (*service.P
165218
}, latencyMs, nil
166219
}
167220

168-
func (s *proxyProbeService) parseHTTPBin(body []byte, latencyMs int64) (*service.ProxyExitInfo, int64, error) {
169-
// httpbin.org/ip 返回格式: {"origin": "1.2.3.4"}
221+
func (s *proxyProbeService) parseIPify(body []byte, latencyMs int64) (*service.ProxyExitInfo, int64, error) {
170222
var result struct {
171-
Origin string `json:"origin"`
223+
IP string `json:"ip"`
172224
}
173225
if err := json.Unmarshal(body, &result); err != nil {
174-
return nil, latencyMs, fmt.Errorf("failed to parse httpbin response: %w", err)
226+
return nil, latencyMs, fmt.Errorf("failed to parse ipify response: %w", err)
175227
}
176-
if result.Origin == "" {
177-
return nil, latencyMs, fmt.Errorf("httpbin: no IP found in response")
228+
if result.IP == "" {
229+
return nil, latencyMs, fmt.Errorf("ipify: no IP found in response")
178230
}
179231
return &service.ProxyExitInfo{
180-
IP: result.Origin,
232+
IP: result.IP,
181233
}, latencyMs, nil
182234
}

‎backend/internal/repository/proxy_probe_service_test.go‎

Lines changed: 69 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -2,9 +2,12 @@ package repository
22

33
import (
44
"context"
5+
"errors"
6+
"fmt"
57
"io"
68
"net/http"
79
"net/http/httptest"
10+
"net/url"
811
"strings"
912
"testing"
1013

@@ -71,24 +74,24 @@ func (s *ProxyProbeServiceSuite) TestProbeProxy_Success_IPAPI() {
7174
require.Equal(s.T(), "CC", info.CountryCode)
7275
}
7376

74-
func (s *ProxyProbeServiceSuite) TestProbeProxy_Success_HTTPBinFallback() {
77+
func (s *ProxyProbeServiceSuite) TestProbeProxy_Success_IPifyFallback() {
7578
s.setupProxyServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
7679
// ip-api 失败
7780
if strings.Contains(r.RequestURI, "ip-api.com") {
7881
w.WriteHeader(http.StatusServiceUnavailable)
7982
return
8083
}
81-
// httpbin 成功
82-
if strings.Contains(r.RequestURI, "httpbin.org") {
84+
// ipify 成功
85+
if strings.Contains(r.RequestURI, "ipify.org") {
8386
w.Header().Set("Content-Type", "application/json")
84-
_, _ = io.WriteString(w, `{"origin": "5.6.7.8"}`)
87+
_, _ = io.WriteString(w, `{"ip": "5.6.7.8"}`)
8588
return
8689
}
8790
w.WriteHeader(http.StatusServiceUnavailable)
8891
}))
8992

9093
info, latencyMs, err := s.prober.ProbeProxy(s.ctx, s.proxySrv.URL)
91-
require.NoError(s.T(), err, "ProbeProxy should fallback to httpbin")
94+
require.NoError(s.T(), err, "ProbeProxy should fallback to ipify")
9295
require.GreaterOrEqual(s.T(), latencyMs, int64(0), "unexpected latency")
9396
require.Equal(s.T(), "5.6.7.8", info.IP)
9497
}
@@ -101,6 +104,13 @@ func (s *ProxyProbeServiceSuite) TestProbeProxy_AllFailed() {
101104
_, _, err := s.prober.ProbeProxy(s.ctx, s.proxySrv.URL)
102105
require.Error(s.T(), err)
103106
require.ErrorContains(s.T(), err, "all probe URLs failed")
107+
108+
// 聚合信息里每个探测点各占一段,且不泄漏完整探测 URL
109+
msg := err.Error()
110+
require.Contains(s.T(), msg, "ip-api: ")
111+
require.Contains(s.T(), msg, "ipify: ")
112+
require.NotContains(s.T(), msg, "http://")
113+
require.Less(s.T(), len(msg), 240, "聚合提示应保持精简: %s", msg)
104114
}
105115

106116
func (s *ProxyProbeServiceSuite) TestProbeProxy_InvalidJSON() {
@@ -110,8 +120,8 @@ func (s *ProxyProbeServiceSuite) TestProbeProxy_InvalidJSON() {
110120
_, _ = io.WriteString(w, "not-json")
111121
return
112122
}
113-
// httpbin 也返回无效响应
114-
if strings.Contains(r.RequestURI, "httpbin.org") {
123+
// ipify 也返回无效响应
124+
if strings.Contains(r.RequestURI, "ipify.org") {
115125
w.Header().Set("Content-Type", "application/json")
116126
_, _ = io.WriteString(w, "not-json")
117127
return
@@ -151,21 +161,67 @@ func (s *ProxyProbeServiceSuite) TestParseIPAPI_Failure() {
151161
require.ErrorContains(s.T(), err, "rate limited")
152162
}
153163

154-
func (s *ProxyProbeServiceSuite) TestParseHTTPBin_Success() {
155-
body := []byte(`{"origin": "9.8.7.6"}`)
156-
info, latencyMs, err := s.prober.parseHTTPBin(body, 50)
164+
func (s *ProxyProbeServiceSuite) TestParseIPify_Success() {
165+
body := []byte(`{"ip": "9.8.7.6"}`)
166+
info, latencyMs, err := s.prober.parseIPify(body, 50)
157167
require.NoError(s.T(), err)
158168
require.Equal(s.T(), int64(50), latencyMs)
159169
require.Equal(s.T(), "9.8.7.6", info.IP)
160170
}
161171

162-
func (s *ProxyProbeServiceSuite) TestParseHTTPBin_NoIP() {
163-
body := []byte(`{"origin": ""}`)
164-
_, _, err := s.prober.parseHTTPBin(body, 50)
172+
func (s *ProxyProbeServiceSuite) TestParseIPify_NoIP() {
173+
body := []byte(`{"ip": ""}`)
174+
_, _, err := s.prober.parseIPify(body, 50)
165175
require.Error(s.T(), err)
166176
require.ErrorContains(s.T(), err, "no IP found")
167177
}
168178

179+
func TestSummarizeProbeError(t *testing.T) {
180+
longReason := strings.Repeat("超时原因", 40)
181+
cases := []struct {
182+
name string
183+
err error
184+
expect string
185+
}{
186+
{"nil", nil, "unknown"},
187+
{"canceled", fmt.Errorf("proxy connection failed: %w", context.Canceled), "canceled"},
188+
{"deadline", fmt.Errorf("proxy connection failed: %w", context.DeadlineExceeded), "timeout"},
189+
{
190+
"net timeout",
191+
&url.Error{Op: "Get", URL: "http://api64.ipify.org?format=json", Err: timeoutErr{}},
192+
"timeout",
193+
},
194+
{
195+
"strips probe url",
196+
fmt.Errorf("proxy connection failed: %w", &url.Error{
197+
Op: "Get",
198+
URL: "http://ip-api.com/json/?lang=zh-CN",
199+
Err: errors.New("connection refused"),
200+
}),
201+
"proxy connection failed: connection refused",
202+
},
203+
{"status code", errors.New("request failed with status: 503"), "request failed with status: 503"},
204+
}
205+
206+
for _, tc := range cases {
207+
t.Run(tc.name, func(t *testing.T) {
208+
require.Equal(t, tc.expect, summarizeProbeError(tc.err))
209+
})
210+
}
211+
212+
t.Run("truncates long reason", func(t *testing.T) {
213+
got := summarizeProbeError(errors.New(longReason))
214+
require.Equal(t, maxProbeReasonLen+1, len([]rune(got)), "应按 rune 截断并追加省略号")
215+
require.True(t, strings.HasSuffix(got, "…"))
216+
})
217+
}
218+
219+
type timeoutErr struct{}
220+
221+
func (timeoutErr) Error() string { return "i/o timeout" }
222+
func (timeoutErr) Timeout() bool { return true }
223+
func (timeoutErr) Temporary() bool { return true }
224+
169225
func TestProxyProbeServiceSuite(t *testing.T) {
170226
suite.Run(t, new(ProxyProbeServiceSuite))
171227
}

0 commit comments

Comments
 (0)