{"record":{"id":"9dee58808ac1bf71","repo":"multica-ai/multica","slug":"wecom-credentials-unverifiable","errorCode":"wecom_credentials_unverifiable","errorMessage":"wecom: could not reach WeCom to verify this bot","messagePattern":"wecom: could not reach WeCom to verify this bot","errorType":"exception","errorClass":null,"httpStatus":503,"severity":"warning","filePath":"server/internal/integrations/wecom/credential_probe.go","lineNumber":60,"sourceCode":"\nimport (\n\t\"context\"\n\t\"encoding/json\"\n\t\"errors\"\n\t\"fmt\"\n\t\"log/slog\"\n)\n\n// ErrCredentialsRejected is WeCom saying the pair is not valid: a wrong\n// secret, a bot that no longer exists, a bot whose API mode is off. It is the\n// answer that must reach the admin, because it is the one they can act on.\nvar ErrCredentialsRejected = errors.New(\"wecom: WeCom rejected this bot id and secret\")\n\n// ErrCredentialsUnverifiable is everything else — the dial failed, the\n// handshake timed out, the network is down. Distinct from rejection on\n// purpose: telling an admin their credentials are wrong when the deployment\n// simply could not reach WeCom sends them to rotate a secret that was fine.\nvar ErrCredentialsUnverifiable = errors.New(\"wecom: could not reach WeCom to verify this bot\")\n\n// rejectionErrCodes are the WeCom global error codes (document/path/90313)\n// documented as a refusal of the credential pair itself — the only answers\n// entitled to tell an admin their Bot ID or secret is wrong.\n//\n// Everything else non-zero is ErrCredentialsUnverifiable, deliberately. WeCom\n// only guarantees that 0 means success; the subscribe path is also under\n// frequency and concurrency protection (45009, 45033), and the platform can\n// fail on its own account. Reading any non-zero code as \"wrong secret\" pushes\n// an admin to rotate a long-connection secret that was fine, and a rotated one\n// cannot be recovered — the exact damage this file exists to prevent. So the\n// list is a whitelist and the default is fail-closed: refuse the install, keep\n// the stored credentials untouched, and say we could not verify.\n//\n// Codes only, never errmsg text: the message is human-facing Chinese prose\n// that WeCom is free to reword.\nvar rejectionErrCodes = map[int]struct{}{\n\t40001: {}, // 不合法的secret参数 — the secret does not match this bot","sourceCodeStart":42,"sourceCodeEnd":78,"githubUrl":"https://github.com/multica-ai/multica/blob/2c0912b6ec764b373d44eeea1e80f0d9f11ab417/server/internal/integrations/wecom/credential_probe.go#L42-L78","documentation":"WeCom credential probe error: the probe could not reach WeCom or got an answer it cannot attribute to the credentials — dial failure, handshake timeout, network down, or any non-zero WeCom error code NOT on the rejection whitelist (e.g. frequency/concurrency protections 45009, 45033). Deliberately distinct from rejection: blaming the credentials would push an admin to rotate a long-connection secret that was fine, and a rotated secret cannot be recovered.","triggerScenarios":"Running the credential probe when the Multica server cannot reach WeCom endpoints (egress blocked, DNS, proxy), or when WeCom answers with a throttling/platform error (45009, 45033) instead of a credential verdict. The whitelist-plus-fail-closed design maps all of these here.","commonSituations":"Server behind a firewall without egress to WeCom; retrying the probe rapidly and tripping rate limits; transient WeCom-side outage; corporate proxy terminating the connection.","solutions":["Verify network egress from the server to WeCom API endpoints (curl the probe URL directly), fix proxy/firewall/DNS, then retry.","If throttled (45009/45033), wait before retrying — do not rotate the secret.","Only treat credentials as wrong when ErrCredentialsRejected is returned; for this error, keep the pair and fix reachability."],"exampleFix":"// before\nerr := probe.Check(ctx, botID, secret)\nif err != nil {\n\trotateSecretPrompt(w) // WRONG: sends admin to rotate a good secret\n}\n\n// after\nerr := probe.Check(ctx, botID, secret)\nif errors.Is(err, wecom.ErrCredentialsUnverifiable) {\n\t// network/platform issue — never suggest rotating; the rotated secret is unrecoverable\n\trespond(w, 502, \"could not reach WeCom to verify — check network and retry later\")\n\treturn\n}","handlingStrategy":"retry","validationCode":"// pre-flight reachability so install fails fast with a clear cause\nif err := probe.ReachWeCom(ctx, 3*time.Second); err != nil {\n\treturn respondBadGateway(w, \"server cannot reach WeCom — fix egress before entering credentials\")\n}","typeGuard":null,"tryCatchPattern":"err := probe.Check(ctx, botID, secret)\nif err != nil {\n\tif errors.Is(err, wecom.ErrCredentialsUnverifiable) {\n\t\t// transport/throttle — retry with backoff; NEVER suggest rotating the secret\n\t\treturn retryWithBackoff(ctx, probe.Check, botID, secret)\n\t}\n\treturn err\n}","preventionTips":["Never map this error to 'wrong credentials' UI copy — a rotated WeCom long-connection secret is unrecoverable.","Space probe attempts out; 45009/45033 are frequency/concurrency protections, not verdicts.","Verify server egress to WeCom endpoints as part of deployment checks."],"tags":["wecom","network","credentials","fail-closed","go"],"backgroundTag":null,"analyzedSha":"2c0912b6ec764b373d44eeea1e80f0d9f11ab417","analyzedAt":"2026-08-15T13:25:18.241Z","schemaVersion":2},"datasetVersion":"2026-08-15T17:31:12.345Z"}