From dfcb8979547178364bd6302af390e2fb6f121235 Mon Sep 17 00:00:00 2001 From: viettranx Date: Mon, 27 Apr 2026 12:00:09 +0700 Subject: [PATCH] fix(http): provider verify ping mode on empty body (#1034) --- internal/http/openapi_spec.json | 4 ++-- internal/http/provider_verify.go | 37 ++++++++++++++++++++++++++------ 2 files changed, 33 insertions(+), 8 deletions(-) diff --git a/internal/http/openapi_spec.json b/internal/http/openapi_spec.json index c046be4a..bbe02cea 100644 --- a/internal/http/openapi_spec.json +++ b/internal/http/openapi_spec.json @@ -543,9 +543,9 @@ "post": { "tags": ["Providers"], "summary": "Verify provider connection", - "description": "Test provider connectivity with a minimal LLM call.", + "description": "Empty body or omitted model = ping mode (provider registered/reachable check). With model = full chat-verify against the upstream provider.", "parameters": [{ "name": "id", "in": "path", "required": true, "schema": { "type": "string", "format": "uuid" } }], - "requestBody": { "content": { "application/json": { "schema": { "type": "object", "properties": { "model": { "type": "string" } } } } } }, + "requestBody": { "required": false, "content": { "application/json": { "schema": { "type": "object", "properties": { "model": { "type": "string", "description": "Optional. Omit (or send empty body) for ping mode." } } } } } }, "responses": { "200": { "description": "Verification result", "content": { "application/json": { "schema": { "type": "object", "properties": { "valid": { "type": "boolean" }, "error": { "type": "string" } } } } } } } } }, diff --git a/internal/http/provider_verify.go b/internal/http/provider_verify.go index 5ffc03a0..48e4e591 100644 --- a/internal/http/provider_verify.go +++ b/internal/http/provider_verify.go @@ -4,6 +4,7 @@ import ( "context" "encoding/json" "errors" + "io" "net/http" "os/exec" "path/filepath" @@ -17,6 +18,14 @@ import ( "github.com/nextlevelbuilder/goclaw/internal/store" ) +// HandleVerifyProviderForTest invokes the verify handler directly without auth +// middleware. Integration tests must inject the desired tenant_id into the +// request context before calling. Production code MUST go through RegisterRoutes +// so the auth/locale/tenant pipeline runs first. +func (h *ProvidersHandler) HandleVerifyProviderForTest(w http.ResponseWriter, r *http.Request) { + h.handleVerifyProvider(w, r) +} + // handleVerifyProvider tests a provider+model combination with a minimal LLM call. // // POST /v1/providers/{id}/verify @@ -33,14 +42,17 @@ func (h *ProvidersHandler) handleVerifyProvider(w http.ResponseWriter, r *http.R var req struct { Model string `json:"model"` } + // Empty body == ping mode (connectivity check only). Truncated/malformed + // JSON still returns 400. io.EOF on Decode unambiguously means no body; + // io.ErrUnexpectedEOF is what truncated JSON returns. if err := json.NewDecoder(http.MaxBytesReader(w, r.Body, 1<<16)).Decode(&req); err != nil { - writeJSON(w, http.StatusBadRequest, map[string]string{"error": i18n.T(locale, i18n.MsgInvalidJSON)}) - return - } - if req.Model == "" { - writeJSON(w, http.StatusBadRequest, map[string]string{"error": i18n.T(locale, i18n.MsgRequired, "model")}) - return + if !errors.Is(err, io.EOF) { + writeJSON(w, http.StatusBadRequest, map[string]string{"error": i18n.T(locale, i18n.MsgInvalidJSON)}) + return + } + // empty body — req.Model stays "" → pingMode below } + pingMode := req.Model == "" // Look up provider record from DB to get the provider name p, err := h.store.GetProvider(r.Context(), id) @@ -51,6 +63,10 @@ func (h *ProvidersHandler) handleVerifyProvider(w http.ResponseWriter, r *http.R // ACP: verify binary exists on the server (no LLM call needed) if p.ProviderType == store.ProviderACP { + if pingMode { + writeJSON(w, http.StatusOK, map[string]any{"valid": true}) + return + } binary := p.APIBase if binary == "" { binary = "claude" @@ -70,6 +86,10 @@ func (h *ProvidersHandler) handleVerifyProvider(w http.ResponseWriter, r *http.R // Claude CLI: validate model alias locally (no LLM call needed) if p.ProviderType == "claude_cli" { + if pingMode { + writeJSON(w, http.StatusOK, map[string]any{"valid": true}) + return + } validModels := map[string]bool{"sonnet": true, "opus": true, "haiku": true} if validModels[req.Model] { writeJSON(w, http.StatusOK, map[string]any{"valid": true}) @@ -92,6 +112,11 @@ func (h *ProvidersHandler) handleVerifyProvider(w http.ResponseWriter, r *http.R return } + if pingMode { + writeJSON(w, http.StatusOK, map[string]any{"valid": true}) + return + } + // Non-chat models (image/video generation) can't be verified via Chat API. // Accept them if the provider is reachable (already validated above). if isNonChatModel(req.Model) {