{"record":{"id":"64494cc0238e187e","repo":"multica-ai/multica","slug":"wecom-credentials-rejected","errorCode":"wecom_credentials_rejected","errorMessage":"wecom: WeCom rejected this bot id and secret","messagePattern":"wecom: WeCom rejected this bot id and secret","errorType":"exception","errorClass":null,"httpStatus":400,"severity":"error","filePath":"server/internal/integrations/wecom/credential_probe.go","lineNumber":54,"sourceCode":"// refused, a rejected install has knocked a live bot offline for nothing.\n// InstallationService.Upsert enforces this: it takes the slot's advisory lock,\n// reads the current owner, and returns the conflict without probing for any\n// live owner other than the caller's own row (see botSlotConflictErr). What\n// reaches the probe is a free slot, a revoked row, an orphan, or a re-install\n// of the caller's own bot — nothing anybody else is connected to.\n\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","sourceCodeStart":36,"sourceCodeEnd":72,"githubUrl":"https://github.com/multica-ai/multica/blob/2c0912b6ec764b373d44eeea1e80f0d9f11ab417/server/internal/integrations/wecom/credential_probe.go#L36-L72","documentation":"WeCom credential probe error: WeCom itself answered that the bot id + secret pair is invalid — wrong secret, a bot that no longer exists, or a bot whose API mode is off. Only WeCom global error codes on the documented rejection whitelist (document/path/90313) produce this; it is the answer an admin can act on.","triggerScenarios":"Calling the credential probe during install with a secret that was rotated, a typo'd Bot ID, a deleted bot, or a bot for which the long-connection API mode was never enabled. WeCom returns a whitelisted rejection code, which the probe maps to ErrCredentialsRejected.","commonSituations":"Secret regenerated in the WeCom console after it was pasted; copied bot id vs secret swapped; admin created the bot but did not enable API mode; trailing whitespace in either field.","solutions":["Re-copy both Bot ID and secret from the WeCom console (trim whitespace) and re-run the probe.","Confirm the bot still exists and its API mode / long-connection capability is enabled in the WeCom admin console.","If the secret was rotated, use the CURRENT value — old secrets stop working immediately after rotation."],"exampleFix":"// before\nerr := probe.Check(ctx, botID, secret)\nif err != nil {\n\tinstallFailed(w, err) // shows generic failure\n}\n\n// after\nerr := probe.Check(ctx, botID, secret)\nswitch {\ncase errors.Is(err, wecom.ErrCredentialsRejected):\n\trespond(w, 400, \"WeCom rejected this bot id and secret — re-copy them and check API mode\")\ncase errors.Is(err, wecom.ErrCredentialsUnverifiable):\n\trespond(w, 502, \"could not reach WeCom — check network, do NOT rotate the secret\")\n}","handlingStrategy":"try-catch","validationCode":"// cheap pre-check: trim inputs, confirm both are non-empty and well-formed\nbotID, secret = strings.TrimSpace(botID), strings.TrimSpace(secret)\nif botID == \"\" || secret == \"\" {\n\treturn respondFieldError(w, \"\", \"both bot id and secret are required\")\n}","typeGuard":null,"tryCatchPattern":"err := probe.Check(ctx, botID, secret)\nif err != nil {\n\tswitch {\n\tcase errors.Is(err, wecom.ErrCredentialsRejected):\n\t\t// admin-actionable: re-copy credentials / enable API mode; do not retry\n\t\treturn respondBadRequest(w, \"WeCom rejected this bot id and secret\")\n\tcase errors.Is(err, wecom.ErrCredentialsUnverifiable):\n\t\treturn respondBadGateway(w, \"could not reach WeCom\")\n\t}\n\treturn err\n}","preventionTips":["Trim whitespace on both fields before probing.","After rotating a WeCom secret, update it everywhere immediately — old values fail with this error.","Confirm the bot's API mode is enabled in WeCom before first connect."],"tags":["wecom","credentials","configuration","validation","go"],"backgroundTag":null,"analyzedSha":"2c0912b6ec764b373d44eeea1e80f0d9f11ab417","analyzedAt":"2026-08-15T13:25:18.241Z","schemaVersion":2},"datasetVersion":"2026-08-15T22:17:37.221Z"}