From adc262a23875c8c2d107a02209633aaf65a162d3 Mon Sep 17 00:00:00 2001 From: MHSanaei Date: Wed, 13 May 2026 21:13:16 +0200 Subject: [PATCH] fix(warp): set license against Cloudflare API and surface errors inline MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The license update was always failing because the Cloudflare response has no `success` field — the check rejected every successful PUT. On real errors (e.g. "Too many connected devices."), the toast leaked the raw URL + JSON body. Now the WARP API's error envelope is parsed into a clean message and shown inline next to the Update button. --- frontend/src/pages/xray/WarpModal.vue | 26 +++++++++++++++++++++++--- web/service/warp.go | 27 ++++++++++++++++++++------- 2 files changed, 43 insertions(+), 10 deletions(-) diff --git a/frontend/src/pages/xray/WarpModal.vue b/frontend/src/pages/xray/WarpModal.vue index c42ede75..0192ebf9 100644 --- a/frontend/src/pages/xray/WarpModal.vue +++ b/frontend/src/pages/xray/WarpModal.vue @@ -27,6 +27,7 @@ const loading = ref(false); const warpData = ref(null); const warpConfig = ref(null); const warpPlus = ref(''); +const licenseError = ref(''); // Held in memory so the parent's add/reset handlers receive the same // object the modal computed from getConfig(). const stagedOutbound = ref(null); @@ -41,6 +42,7 @@ watch(() => props.open, (next) => { if (!next) return; warpConfig.value = null; stagedOutbound.value = null; + licenseError.value = ''; fetchData(); }); @@ -89,12 +91,15 @@ async function getConfig() { async function updateLicense() { if (warpPlus.value.length < 26) return; loading.value = true; + licenseError.value = ''; try { const msg = await HttpUtil.post('/panel/xray/warp/license', { license: warpPlus.value }); if (msg?.success) { warpData.value = JSON.parse(msg.obj); warpConfig.value = null; warpPlus.value = ''; + } else { + licenseError.value = msg?.msg || 'Failed to set WARP license.'; } } finally { loading.value = false; @@ -233,9 +238,12 @@ const hasConfig = computed(() => !ObjectUtil.isEmpty(warpConfig.value)); - - Update + +
+ Update + +
@@ -358,4 +366,16 @@ const hasConfig = computed(() => !ObjectUtil.isEmpty(warpConfig.value)); .ml-8 { margin-left: 8px; } + +.license-actions { + display: flex; + align-items: center; + gap: 8px; + flex-wrap: wrap; +} + +.license-error { + flex: 1; + min-width: 0; +} diff --git a/web/service/warp.go b/web/service/warp.go index 6d36774a..fbe39957 100644 --- a/web/service/warp.go +++ b/web/service/warp.go @@ -152,13 +152,8 @@ func (s *WarpService) SetWarpLicense(license string) (string, error) { if err := json.Unmarshal(body, &response); err != nil { return "", err } - if success, _ := response["success"].(bool); !success { - if errorArr, ok := response["errors"].([]any); ok && len(errorArr) > 0 { - if errorObj, ok := errorArr[0].(map[string]any); ok { - return "", common.NewError(errorObj["code"], errorObj["message"]) - } - } - return "", common.NewError("warp set license failed: unknown error") + if _, ok := response["id"].(string); !ok { + return "", common.NewErrorf("warp set license failed: unexpected response: %s", string(body)) } warpData["license_key"] = license @@ -202,8 +197,26 @@ func doWarpRequest(req *http.Request) ([]byte, error) { return nil, err } if resp.StatusCode < 200 || resp.StatusCode >= 300 { + if msg := parseWarpError(body); msg != "" { + return nil, common.NewError(msg) + } return nil, common.NewErrorf("warp api %s %s returned status %d: %s", req.Method, req.URL.Path, resp.StatusCode, string(body)) } return body, nil } + +func parseWarpError(body []byte) string { + var env struct { + Errors []struct { + Message string `json:"message"` + } `json:"errors"` + } + if err := json.Unmarshal(body, &env); err != nil { + return "" + } + if len(env.Errors) == 0 || env.Errors[0].Message == "" { + return "" + } + return env.Errors[0].Message +}