fix(runtime): M52 把 *url.Error 直打的 19 处换成 trace.URLCause
model / multimodal / tts / embedding 里所有「把 net/http 的 *url.Error 直接打进 错误串」的地方(M51 清单的 B1 组 17 处 + 清单外的同根因 2 处)统一改走 trace.URLCause:错误串只少一段地址,底层原因、哨兵分类、状态码、重试/降级判据 一字不动。 清单内(17): - model/openai.go: Chat / ChatStream 的 NewRequest、doRequest - model/anthropic.go: chatOnce / streamOnce 的 NewRequest、doRequest - multimodal/vision/adapter.go: ask 的 NewRequest、client.Do - multimodal/voice/cloud.go: postJSON 的 NewRequest、client.Do - multimodal/voice/dashscope.go: download(预签名 OSS 地址)与 dashscopePost 各两处 - voice/tts/client.go: unreachable(返回值与同一行日志的 err 字段都只留原因, 地址只由 redactURL(endpoint) 出现一次) - embedding/api.go: embedChunk 的 client.Do、newEmbeddingRequest 的 NewRequest 清单外同根因(2,均为 %w 形态,B1 清单按 %v 搜时漏掉): - voice/tts/client.go: speak 的 NewRequest 失败 - embedding/endpoint.go: normalizeEndpointURL 的 url.Parse 失败 测试:5 个新文件 18 个用例,逐处可变异打红(实测:把任一处的 URLCause 去掉 ⇒ 对应用例红);dashscope 预签名地址单独一条用例,并另有一条钉住 403 状态码仍在。
This commit is contained in:
1 parent
2702777950
commit
969857682f
13 files changed
+657
-20
No files matched your search
@@ -231,7 +231,7 @@ func (e *APIEmbedder) embedChunk(ctx context.Context, texts []string) ([]Vector,
|
||||
|
||||
resp, err := e.client.Do(req)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("embedding: request: %w", err)
|
||||
return nil, fmt.Errorf("embedding: request: %w", trace.URLCause(err))
|
||||
}
|
||||
defer func() { _ = resp.Body.Close() }()
|
||||
|
||||
@@ -298,7 +298,7 @@ func (e *APIEmbedder) newEmbeddingRequest(ctx context.Context, texts []string) (
|
||||
|
||||
req, err := http.NewRequestWithContext(ctx, http.MethodPost, endpoint, bytes.NewReader(payload))
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("embedding: build request: %w", err)
|
||||
return nil, fmt.Errorf("embedding: build request: %w", trace.URLCause(err))
|
||||
}
|
||||
req.Header.Set("Content-Type", "application/json")
|
||||
if e.cfg.APIKey != "" {
|
||||
|
||||
@@ -5,6 +5,8 @@ import (
|
||||
"net"
|
||||
"net/url"
|
||||
"strings"
|
||||
|
||||
"git.yeij.top/AskaEth/Cyrene_v2/internal/runtime/trace"
|
||||
)
|
||||
|
||||
// 本文件只管**端点地址的规范化**:把「连接语义上不可靠」的回环主机名别名改写
|
||||
@@ -39,7 +41,8 @@ const loopbackIPv4 = "127.0.0.1"
|
||||
func normalizeEndpointURL(raw string) (string, error) {
|
||||
u, err := url.Parse(raw)
|
||||
if err != nil {
|
||||
return "", fmt.Errorf("embedding: parse api url: %w", err)
|
||||
// 不回原文:url.Parse 的 *url.Error 会把整条地址(query 里可能是 api_key)渲染进去。
|
||||
return "", fmt.Errorf("embedding: parse api url: %w", trace.URLCause(err))
|
||||
}
|
||||
if u.Scheme != "http" && u.Scheme != "https" {
|
||||
// 不是本客户端能拨的 scheme:原样返回,让后续的报错如实暴露配置问题,
|
||||
|
||||
@@ -0,0 +1,89 @@
|
||||
package embedding
|
||||
|
||||
// 本文件是 M52 的出口回归探针:embedding 客户端里「把 net/http 的 *url.Error 直接
|
||||
// 打进错误串」的两处 —— `embedChunk` 的传输失败与 `normalizeEndpointURL` 的解析失败 ——
|
||||
// 一律先过 trace.URLCause。
|
||||
//
|
||||
// 为什么这两处特别敏感:/embeddings 的端点常常配成带 `?api_key=` 的网关地址,而
|
||||
// `client.Do` 失败时 net/http 会把**整条地址**渲染进错误串(密码被打码、用户名与 query
|
||||
// 不打码)。原文进日志就等于把网关凭据交出去。
|
||||
//
|
||||
// 这一包与 model/vision 的差别:这里原本用的是 `%w`(不是 `%v`),因此除了「去地址」,
|
||||
// 还必须钉住 `%w` 的链没被削弱 —— 把 URLCause 换成「新建一个错误」会让
|
||||
// errors.Is(err, context.DeadlineExceeded) 变红。
|
||||
|
||||
import (
|
||||
"context"
|
||||
"errors"
|
||||
"fmt"
|
||||
"net/http"
|
||||
"strings"
|
||||
"testing"
|
||||
|
||||
"git.yeij.top/AskaEth/Cyrene_v2/internal/config"
|
||||
)
|
||||
|
||||
const (
|
||||
urlCauseQuerySecret = "SECRET-2f8a1c"
|
||||
urlCauseUser = "svc-user"
|
||||
urlCauseBadHost = "bad host.example.com"
|
||||
)
|
||||
|
||||
func embedURLCauseGoodURL() string {
|
||||
return "https://" + urlCauseUser + ":svc-pass@embed.example.com/v1/embeddings?api_key=" + urlCauseQuerySecret
|
||||
}
|
||||
|
||||
func embedURLCauseBadURL() string {
|
||||
return "https://" + urlCauseUser + ":svc-pass@" + urlCauseBadHost + "/v1/embeddings?api_key=" + urlCauseQuerySecret
|
||||
}
|
||||
|
||||
// TestEmbedDropsURLFromTransportError 钉住 embedChunk 里 `e.client.Do` 失败那一处,
|
||||
// 并钉住 `%w` 的哨兵链仍然成立(context.DeadlineExceeded 可 errors.Is)。
|
||||
func TestEmbedDropsURLFromTransportError(t *testing.T) {
|
||||
t.Parallel()
|
||||
emb := NewAPI(
|
||||
config.EmbeddingConfig{APIURL: embedURLCauseGoodURL()},
|
||||
WithRoundTripper(roundTripFunc(func(*http.Request) (*http.Response, error) {
|
||||
return nil, fmt.Errorf("net/http: %w", context.DeadlineExceeded)
|
||||
})),
|
||||
)
|
||||
|
||||
_, err := emb.Embed(context.Background(), []string{"hi"})
|
||||
if err == nil {
|
||||
t.Fatal("want error, got nil")
|
||||
}
|
||||
if strings.Contains(err.Error(), urlCauseQuerySecret) {
|
||||
t.Fatalf("错误串带出了 query 里的凭据:%v", err)
|
||||
}
|
||||
if strings.Contains(err.Error(), urlCauseUser) {
|
||||
t.Fatalf("错误串带出了 userinfo 里的用户名:%v", err)
|
||||
}
|
||||
if !strings.Contains(err.Error(), "deadline exceeded") {
|
||||
t.Fatalf("错误串丢了底层原因(只许少地址):%v", err)
|
||||
}
|
||||
// `%w` 链必须还在:URLCause 只剥 *url.Error 外壳,内层原因原样返回。
|
||||
if !errors.Is(err, context.DeadlineExceeded) {
|
||||
t.Fatalf("err = %v, want errors.Is(context.DeadlineExceeded)(哨兵链不许被带偏)", err)
|
||||
}
|
||||
}
|
||||
|
||||
// TestEmbedDropsURLFromParseError 钉住 normalizeEndpointURL 的解析失败:
|
||||
// url.Parse 的 *url.Error 里就是整条 api URL。
|
||||
func TestEmbedDropsURLFromParseError(t *testing.T) {
|
||||
t.Parallel()
|
||||
emb := NewAPI(config.EmbeddingConfig{APIURL: embedURLCauseBadURL()})
|
||||
|
||||
_, err := emb.Embed(context.Background(), []string{"hi"})
|
||||
if err == nil {
|
||||
t.Fatal("want error, got nil")
|
||||
}
|
||||
if strings.Contains(err.Error(), urlCauseQuerySecret) || strings.Contains(err.Error(), urlCauseUser) {
|
||||
t.Fatalf("解析失败的串带出了 api URL 里的凭据:%v", err)
|
||||
}
|
||||
if !strings.Contains(err.Error(), "invalid character") {
|
||||
t.Fatalf("错误串丢了底层解析原因(只许少地址):%v", err)
|
||||
}
|
||||
if !strings.Contains(err.Error(), "parse api url") {
|
||||
t.Fatalf("错误串丢了来源标记:%v", err)
|
||||
}
|
||||
}
|
||||
Reference in new issue
Block a user