{"record":{"id":"7e081a5c102ef600","repo":"signalapp/Signal-Server","slug":"failed-to-make-http-request-to-cloudflare-turn","errorCode":null,"errorMessage":"failed to make http request to Cloudflare Turn: {}","messagePattern":"failed to make http request to Cloudflare Turn: (.+?)","errorType":"http","errorClass":"IOException","httpStatus":503,"severity":"error","filePath":"service/src/main/java/org/whispersystems/textsecuregcm/auth/CloudflareTurnCredentialsManager.java","lineNumber":138,"sourceCode":"      turnHostname = this.cloudflareTurnHostname;\n    }\n    try {\n      cloudflareTurnComposedUrls = dnsNameResolver.resolveAll(turnHostname).get().stream()\n          .map(i -> switch (i) {\n            case Inet6Address i6 -> \"[\" + i6.getHostAddress() + \"]\";\n            default -> i.getHostAddress();\n          })\n          .flatMap(i -> turnUrlsWithIps.stream().map(u -> u.formatted(i)))\n          .toList();\n    } catch (Exception e) {\n      throw new IOException(e);\n    }\n\n    final HttpResponse<String> response;\n    try {\n      response = cloudflareTurnClient.sendAsync(getCredentialsRequest, HttpResponse.BodyHandlers.ofString()).join();\n    } catch (CompletionException e) {\n      logger.warn(\"failed to make http request to Cloudflare Turn: {}\", e.getMessage());\n      throw new IOException(ExceptionUtils.unwrap(e));\n    }\n\n    if (response.statusCode() != Response.Status.CREATED.getStatusCode()) {\n      logger.warn(\"failure request credentials from Cloudflare Turn (code={}): {}\", response.statusCode(), response);\n      throw new IOException(\"Cloudflare Turn http failure : \" + response.statusCode());\n    }\n\n    final CloudflareTurnResponse cloudflareTurnResponse = SystemMapper.jsonMapper()\n        .readValue(response.body(), CloudflareTurnResponse.class);\n\n    return new TurnToken(\n        cloudflareTurnResponse.iceServers().username(),\n        cloudflareTurnResponse.iceServers().credential(),\n        clientCredentialTtl.toSeconds(),\n        turnUrls,\n        cloudflareTurnComposedUrls,\n        turnHostname","sourceCodeStart":120,"sourceCodeEnd":156,"githubUrl":"https://github.com/signalapp/Signal-Server/blob/100ab61c82627582c867d19e1c0561ba2781e927/service/src/main/java/org/whispersystems/textsecuregcm/auth/CloudflareTurnCredentialsManager.java#L120-L156","documentation":"This is the request-transport failure branch of the same Cloudflare Turn credential fetch. The CompletableFuture from HttpClient.sendAsync is joined; if the underlying request failed (connection error, timeout, TLS problem), join() wraps it in a CompletionException, which is unwrapped and rethrown as an IOException. The log line records only e.getMessage() of the wrapper.","triggerScenarios":"cloudflareTurnClient.sendAsync(...).join() throws CompletionException because the HTTP call could not complete: DNS failure, connection refused/timeout, TLS handshake failure, or an interrupted join.","commonSituations":"Egress network restrictions from the server, Cloudflare endpoint unreachable, misconfigured proxy or firewall, DNS resolution problems, or overly short HttpClient connect timeouts.","solutions":["Check outbound connectivity from the server to the Cloudflare Turn endpoint (curl the URL from the host).","Inspect ExceptionUtils.unwrap(e) in the thrown IOException for the root cause (ConnectException, HttpTimeoutException, SSLHandshakeException).","Increase HttpClient connect/request timeouts if the failure is a timeout.","Add bounded retries with backoff for transient network errors before surfacing the IOException to callers."],"exampleFix":"// before\nresponse = cloudflareTurnClient.sendAsync(req, HttpResponse.BodyHandlers.ofString()).join();\n\n// after: tolerate transient network failures\nfor (int attempt = 0; attempt < 3; attempt++) {\n  try {\n    response = cloudflareTurnClient.sendAsync(req, HttpResponse.BodyHandlers.ofString()).join();\n    break;\n  } catch (CompletionException e) {\n    if (attempt == 2) throw new IOException(ExceptionUtils.unwrap(e));\n    Uninterruptibles.sleepUninterruptibly(1L << attempt, TimeUnit.SECONDS);\n  }\n}","handlingStrategy":"retry","validationCode":"// pre-call reachability probe (startup health check)\nHttpRequest probe = HttpRequest.newBuilder(URI.create(cloudflareTurnUrl)).method(\"HEAD\", HttpRequest.BodyPublishers.noBody()).build();\nboolean reachable = cloudflareTurnClient.sendAsync(probe, HttpResponse.BodyHandlers.discarding()).isDone();","typeGuard":null,"tryCatchPattern":"try {\n  creds = manager.retrieveFromCloudflare(realm, keyId, ttlSeconds);\n} catch (IOException e) {\n  Throwable root = ExceptionUtils.getRootCause(e);\n  if (root instanceof HttpTimeoutException || root instanceof ConnectException) {\n    // retry with backoff or serve cached credentials\n  }\n}","preventionTips":["Configure explicit HttpClient connect and request timeouts and alert on their breach.","Ensure egress firewall/proxy rules allow the Cloudflare Turn endpoint from production hosts.","Wrap the join() call with bounded retries for transient CompletionExceptions."],"tags":["network","http-client","cloudflare","timeout"],"backgroundTag":"network-request-failed","analyzedSha":"100ab61c82627582c867d19e1c0561ba2781e927","analyzedAt":"2026-09-09T13:29:47.883Z","contentChangedAt":"2026-09-09T13:29:47.883Z","schemaVersion":2},"datasetVersion":"2026-09-16T09:17:16.951Z"}