{"record":{"id":"55a3304acbe3bcdd","repo":"toeverything/AFFiNE","slug":"network-error","errorCode":"network_error","errorMessage":"Captcha verification temporarily unavailable","messagePattern":"Captcha verification temporarily unavailable","errorType":"exception","errorClass":"NetworkError","httpStatus":504,"severity":"error","filePath":"packages/backend/server/src/plugins/captcha/service.ts","lineNumber":80,"sourceCode":"    formData.append('secret', this.captcha.turnstile.secret);\n    formData.append('response', token);\n    formData.append('remoteip', ip);\n    formData.append('idempotency_key', randomUUID());\n\n    const url = 'https://challenges.cloudflare.com/turnstile/v0/siteverify';\n    let result: Response;\n    try {\n      result = await fetch(url, {\n        body: formData,\n        method: 'POST',\n        signal: AbortSignal.timeout(5000),\n      });\n    } catch {\n      metrics.auth.counter('captcha_verification').add(1, {\n        provider: 'turnstile',\n        result: 'unavailable',\n      });\n      throw new NetworkError('Captcha verification temporarily unavailable');\n    }\n    if (!result.ok) {\n      metrics.auth.counter('captcha_verification').add(1, {\n        provider: 'turnstile',\n        result: 'unavailable',\n      });\n      throw new NetworkError('Captcha verification temporarily unavailable');\n    }\n    let parsed: z.SafeParseReturnType<\n      unknown,\n      z.infer<typeof turnstileResponse>\n    >;\n    try {\n      parsed = turnstileResponse.safeParse(await result.json());\n    } catch {\n      parsed = turnstileResponse.safeParse(null);\n    }\n    if (!parsed.success) {","sourceCodeStart":62,"sourceCodeEnd":98,"githubUrl":"https://github.com/toeverything/AFFiNE/blob/b4c8548c09da21b2898443559a5b846f0ccf5dd8/packages/backend/server/src/plugins/captcha/service.ts#L62-L98","documentation":"Thrown when the outbound fetch to Cloudflare Turnstile's siteverify endpoint fails outright — DNS failure, TCP/TLS error, or the 5-second AbortSignal.timeout firing. The captcha service treats any inability to reach Turnstile as a temporary network condition, records the 'unavailable' metric, and raises NetworkError so callers can retry rather than blame the user.","triggerScenarios":"Server has no outbound internet access (air-gapped or egress-firewalled self-host); Turnstile endpoint blocked by proxy rules; slow networks or Turnstile outages causing the 5s AbortSignal.timeout to abort; DNS resolution failures in the container.","commonSituations":"Self-hosted instances behind corporate proxies that whitelist only specific domains; Kubernetes pods with broken DNS; transient Cloudflare incidents; CI environments without network.","solutions":["Verify outbound HTTPS connectivity to challenges.cloudflare.com/turnstile/v0/siteverify from the server host (curl test).","If a proxy is required, configure the standard proxy env vars / global agent so Node's fetch uses it.","Fix container DNS (resolv.conf, CoreDNS) if the host resolves but the container does not.","Retry the request after a short backoff — this error is explicitly 'temporarily unavailable'; if it persists, check Cloudflare status."],"exampleFix":"null","handlingStrategy":"retry","validationCode":"await fetch('https://challenges.cloudflare.com/turnstile/v0/siteverify', { method: 'HEAD', signal: AbortSignal.timeout(3000) })\n  .then(() => console.log('turnstile reachable'))\n  .catch(() => console.error('turnstile unreachable — fix egress/DNS before enabling captcha'));","typeGuard":"const isNetworkError = (e: unknown): e is { code: 'network_error' } =>\n  !!e && typeof e === 'object' && (e as any).code === 'network_error';","tryCatchPattern":"for (let attempt = 1; attempt <= 3; attempt++) {\n  try {\n    return await captchaService.verifyRequest(credential, req);\n  } catch (e) {\n    if (!isNetworkError(e) || attempt === 3) throw e;\n    await backoff(attempt * 500);\n  }\n}","preventionTips":["Smoke-test outbound access to challenges.cloudflare.com during deployment.","Configure proxy env vars for egress-controlled networks.","Alert on the captcha_verification{result=unavailable} metric to catch connectivity regressions."],"tags":["network","captcha","turnstile","timeout","outbound-http"],"backgroundTag":"upstream-service-unreachable","analyzedSha":"b4c8548c09da21b2898443559a5b846f0ccf5dd8","analyzedAt":"2026-08-18T21:16:52.546Z","contentChangedAt":"2026-08-18T21:16:52.546Z","schemaVersion":2},"datasetVersion":"2026-09-14T00:17:10.932Z"}