From 7675e2cd886313382650393d3e8ca061ffa2fc4d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E5=A7=9A=E7=AB=A0=E6=B5=A9?= Date: Thu, 5 Nov 2026 12:03:42 +0800 Subject: [PATCH] =?UTF-8?q?fix(ui):=20=E4=BF=9D=E7=95=99=E4=B8=9A=E5=8A=A1?= =?UTF-8?q?HTTP=E9=85=8D=E7=BD=AE=E8=B6=85=E6=97=B6?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit F13 动态客户端原先把 TEST_WEKNORA_HTTP_TIMEOUT(默认 60s)强制截成 10s, 与 readiness 探针上限混用,导致上传/轮询等业务请求 10s 即超时。 现在显式正超时原样传入;未配置/非正值回落到 appconfig 的 60s 默认。 readiness 探测自己的 10s 上限不变。 新增本地 httptest:60s 配置下 11s 响应成功,显式 50ms 短超时仍生效; 端点/凭据/绑定动态解析回归保持通过。make verify 17 项通过。 --- CHANGELOG.md | 9 +++++ internal/weknoraaccess/client.go | 8 +++- internal/weknoraaccess/client_test.go | 57 +++++++++++++++++++++++++++ 3 files changed, 72 insertions(+), 2 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 029f621..3108286 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,14 @@ # Changelog +## 开发快照 0.1.358(业务 HTTP 超时不再被探针上限截断) + +### Fixed + +- **F13 超时回归**:`weknoraaccess.Client` 不再把显式配置的 + `TEST_WEKNORA_HTTP_TIMEOUT`(默认 60s)强制截成 10s。只有未配置/非正值才回落到 + 与原 appconfig 一致的 60s;readiness 探测自身的 10s 上限保持独立。11s 本地响应在 + 60s 配置下可完成,显式 50ms 短超时仍生效。 + ## 开发快照 0.1.357(取件后自动解析、定位一次性请求、worker 有效配置) ### Fixed diff --git a/internal/weknoraaccess/client.go b/internal/weknoraaccess/client.go index d7524d4..2ce9dab 100644 --- a/internal/weknoraaccess/client.go +++ b/internal/weknoraaccess/client.go @@ -11,6 +11,10 @@ import ( var ErrNotConfigured = errors.New("WeKnora 有效端点/凭据未配置") +// defaultBusinessTimeout matches appconfig's TEST_WEKNORA_HTTP_TIMEOUT default. +// The readiness probe has its own 10s cap; it must not bound upload/poll calls. +const defaultBusinessTimeout = 60 * time.Second + // Client resolves the effective WeKnora target on every call. It deliberately // owns no cached *weknora.Client: endpoint, key and binding are console-managed. type Client struct { @@ -28,8 +32,8 @@ func (client *Client) with(ctx context.Context, call func(*weknora.Client) error return ErrNotConfigured } timeout := client.Timeout - if timeout <= 0 || timeout > 10*time.Second { - timeout = 10 * time.Second + if timeout <= 0 { + timeout = defaultBusinessTimeout } remote, err := weknora.NewClient(weknora.ClientConfig{ BaseURL: target.BaseURL, diff --git a/internal/weknoraaccess/client_test.go b/internal/weknoraaccess/client_test.go index 54be5bf..6faedc2 100644 --- a/internal/weknoraaccess/client_test.go +++ b/internal/weknoraaccess/client_test.go @@ -51,3 +51,60 @@ func TestDynamicClientReResolvesEndpointKeyAndBinding(t *testing.T) { t.Fatalf("rotated endpoint/key = %#v, %v", knowledge, err) } } + +// F13 timeout regression: the configured business timeout (default 60s) must +// reach upload/poll clients unchanged; only a missing/zero value falls back. +// The readiness probe's 10s cap must not leak into business HTTP. +func TestConfiguredBusinessTimeoutIsNotCappedToProbeTimeout(t *testing.T) { + server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + select { + case <-time.After(11 * time.Second): + _, _ = w.Write([]byte(`{"success":true,"data":{"id":"slow-but-valid"}}`)) + case <-r.Context().Done(): + } + })) + defer server.Close() + client := &Client{ + Resolver: readiness.Resolver{ + Endpoint: func(context.Context) string { return server.URL }, + ReadKey: func() (string, error) { return "isolated-test-key", nil }, + DefaultKB: "isolated-test-kb", + }, + Timeout: 60 * time.Second, + } + started := time.Now() + item, err := client.GetKnowledge(context.Background(), "slow-but-valid") + if err != nil { + t.Fatalf("60s business timeout rejected 11s response after %s: %v", time.Since(started), err) + } + if item.ID != "slow-but-valid" { + t.Fatalf("wrong response identity: %q", item.ID) + } +} + +// An explicit short timeout is still honoured for callers that need it. +func TestExplicitShortBusinessTimeoutStillApplies(t *testing.T) { + server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + select { + case <-time.After(300 * time.Millisecond): + _, _ = w.Write([]byte(`{"success":true,"data":{"id":"too-late"}}`)) + case <-r.Context().Done(): + } + })) + defer server.Close() + client := &Client{ + Resolver: readiness.Resolver{ + Endpoint: func(context.Context) string { return server.URL }, + ReadKey: func() (string, error) { return "isolated-test-key", nil }, + DefaultKB: "isolated-test-kb", + }, + Timeout: 50 * time.Millisecond, + } + started := time.Now() + if _, err := client.GetKnowledge(context.Background(), "too-late"); err == nil { + t.Fatal("explicit 50ms timeout did not bound the call") + } + if elapsed := time.Since(started); elapsed > 2*time.Second { + t.Fatalf("explicit short timeout took %s", elapsed) + } +}