From 1ceeb7e7e0910d817be0490d542c784eee7aa6cc Mon Sep 17 00:00:00 2001 From: "2569718930@qq.com" <2569718930@qq.com> Date: Sat, 13 Jun 2026 22:09:11 +0800 Subject: [PATCH] fix wallet verify proxy resilience --- .../app/api/payments/wallets/verify/route.ts | 86 +++++++++++++++---- .../account/__tests__/paymentSecurity.test.ts | 20 +++++ frontend/components/account/useWalletBind.ts | 14 ++- 3 files changed, 102 insertions(+), 18 deletions(-) diff --git a/frontend/app/api/payments/wallets/verify/route.ts b/frontend/app/api/payments/wallets/verify/route.ts index f691af27..410e5e7a 100644 --- a/frontend/app/api/payments/wallets/verify/route.ts +++ b/frontend/app/api/payments/wallets/verify/route.ts @@ -10,6 +10,42 @@ import { } from "@/lib/api-proxy"; const API_BASE = process.env.POLYWEATHER_API_BASE_URL; +const BACKEND_RETRY_DELAY_MS = 250; + +function sleep(ms: number) { + return new Promise((resolve) => setTimeout(resolve, ms)); +} + +async function fetchWalletVerifyWithRetry( + url: string, + init: RequestInit, + attempts = 2, +) { + let lastError: unknown; + for (let attempt = 1; attempt <= attempts; attempt += 1) { + try { + return await fetch(url, init); + } catch (error) { + lastError = error; + if (attempt >= attempts) break; + await sleep(BACKEND_RETRY_DELAY_MS * attempt); + } + } + throw lastError; +} + +function logWalletVerifyProxyError( + error: unknown, + authDebug: Record, +) { + const source = error as { name?: unknown; message?: unknown; cause?: unknown }; + console.error("[payment-wallet-verify-proxy-exception]", { + error_name: String(source?.name || "Error"), + error_message: String(source?.message || error || "unknown"), + error_cause: source?.cause ? String(source.cause) : "", + ...authDebug, + }); +} export async function POST(req: NextRequest) { if (!API_BASE) { @@ -18,31 +54,45 @@ export async function POST(req: NextRequest) { { status: 500 }, ); } + let body: unknown; + try { + body = await req.json(); + } catch { + return NextResponse.json( + { error: "Invalid wallet verify request" }, + { status: 400 }, + ); + } + let authDebug: Record = {}; try { - const body = await req.json(); const auth = await buildBackendRequestHeaders(req); const authError = requireBackendPaymentAuth(auth); if (authError) return authError; const proxiedHeaders = new Headers(auth.headers); proxiedHeaders.set("Content-Type", "application/json"); - const res = await fetch(`${API_BASE}/api/payments/wallets/verify`, { - method: "POST", - headers: proxiedHeaders, - body: JSON.stringify(body ?? {}), - cache: "no-store", - }); + authDebug = { + incoming_has_authorization: Boolean( + String(req.headers.get("authorization") || "").trim(), + ), + has_authorization: proxiedHeaders.has("authorization"), + has_entitlement: proxiedHeaders.has("x-polyweather-entitlement"), + has_forwarded_user_id: proxiedHeaders.has("x-polyweather-auth-user-id"), + has_forwarded_email: proxiedHeaders.has("x-polyweather-auth-email"), + }; + const res = await fetchWalletVerifyWithRetry( + `${API_BASE}/api/payments/wallets/verify`, + { + method: "POST", + headers: proxiedHeaders, + body: JSON.stringify(body ?? {}), + cache: "no-store", + }, + ); if (!res.ok) { const raw = await res.text(); const response = buildUpstreamErrorResponse(res.status, raw, { detailLimit: 350, - extraDebug: { - has_authorization: proxiedHeaders.has("authorization"), - has_entitlement: proxiedHeaders.has("x-polyweather-entitlement"), - has_forwarded_user_id: proxiedHeaders.has( - "x-polyweather-auth-user-id", - ), - has_forwarded_email: proxiedHeaders.has("x-polyweather-auth-email"), - }, + extraDebug: authDebug, }); return applyAuthResponseCookies(response, auth.response); } @@ -50,8 +100,12 @@ export async function POST(req: NextRequest) { const response = NextResponse.json(data); return applyAuthResponseCookies(response, auth.response); } catch (error) { + logWalletVerifyProxyError(error, authDebug); return buildProxyExceptionResponse(error, { - publicMessage: "Failed to verify wallet binding", + status: 502, + publicMessage: + "Wallet verify service is temporarily unavailable. Please retry.", + extra: { retryable: true }, }); } } diff --git a/frontend/components/account/__tests__/paymentSecurity.test.ts b/frontend/components/account/__tests__/paymentSecurity.test.ts index a906bec6..31ba94bd 100644 --- a/frontend/components/account/__tests__/paymentSecurity.test.ts +++ b/frontend/components/account/__tests__/paymentSecurity.test.ts @@ -243,10 +243,30 @@ export function runTests() { walletChallengeRouteSource.includes("retryable: true"), "wallet challenge proxy exception response must mark transient failures as retryable", ); + const walletVerifyRouteSource = fs.readFileSync( + path.join(projectRoot, "app/api/payments/wallets/verify/route.ts"), + "utf8", + ); + assert( + walletVerifyRouteSource.includes("fetchWalletVerifyWithRetry"), + "wallet verify proxy must retry a transient backend connection failure before blocking wallet binding", + ); + assert( + walletVerifyRouteSource.includes("Invalid wallet verify request"), + "wallet verify proxy must return a client error for malformed JSON instead of a generic payment failure", + ); + assert( + walletVerifyRouteSource.includes("retryable: true"), + "wallet verify proxy exception response must mark transient failures as retryable", + ); assert( walletBindSource.includes("readPaymentApiErrorMessage"), "wallet binding errors must show the API error message instead of raw JSON", ); + assert( + walletBindSource.includes("钱包验证服务暂时不可用"), + "wallet binding HTML/50x fallback should stay localized in the Chinese account UI", + ); assert( paymentUtilsSource.includes("looksLikeHtmlDocument") && paymentUtilsSource.includes("Payment service is temporarily unavailable") && diff --git a/frontend/components/account/useWalletBind.ts b/frontend/components/account/useWalletBind.ts index 502feb29..d4827f80 100644 --- a/frontend/components/account/useWalletBind.ts +++ b/frontend/components/account/useWalletBind.ts @@ -304,7 +304,12 @@ export function useWalletBind(params: UseWalletBindParams) { method: "POST", headers: authHeaders, body: JSON.stringify({ address }), }); if (!challengeRes.ok) { - const message = await readPaymentApiErrorMessage(challengeRes); + const message = await readPaymentApiErrorMessage( + challengeRes, + isEn + ? "Wallet challenge service is temporarily unavailable." + : "钱包验证服务暂时不可用。", + ); throw new Error(copy.challengeFailed.replace("{raw}", message)); } @@ -318,7 +323,12 @@ export function useWalletBind(params: UseWalletBindParams) { method: "POST", headers: authHeaders, body: JSON.stringify({ address, nonce, signature }), }); if (!verifyRes.ok) { - const message = await readPaymentApiErrorMessage(verifyRes); + const message = await readPaymentApiErrorMessage( + verifyRes, + isEn + ? "Wallet verify service is temporarily unavailable." + : "钱包验证服务暂时不可用。", + ); throw new Error(copy.verifyFailedRaw.replace("{raw}", message)); }