{"record":{"id":"65446056d2472167","repo":"signalapp/Signal-Server","slug":"failure-request-credentials-from-cloudflare-turn","errorCode":null,"errorMessage":"failure request credentials from Cloudflare Turn (code={}): {}","messagePattern":"failure request credentials from Cloudflare Turn \\(code=(.+?)\\): (.+?)","errorType":"http","errorClass":"IOException","httpStatus":503,"severity":"error","filePath":"service/src/main/java/org/whispersystems/textsecuregcm/auth/CloudflareTurnCredentialsManager.java","lineNumber":143,"sourceCode":"            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\n    );\n  }\n}\n","sourceCodeStart":125,"sourceCodeEnd":160,"githubUrl":"https://github.com/signalapp/Signal-Server/blob/100ab61c82627582c867d19e1c0561ba2781e927/service/src/main/java/org/whispersystems/textsecuregcm/auth/CloudflareTurnCredentialsManager.java#L125-L160","documentation":"CloudflareTurnCredentialsManager.retrieveFromCloudflare calls the Cloudflare Turn token API via java.net.http and expects HTTP 201 Created. Any other status code is logged and rethrown as an IOException('Cloudflare Turn http failure : <code>'). Callers of getCredentials therefore see an IOException whenever Cloudflare refuses or errors on the credential request.","triggerScenarios":"The async HTTP request to Cloudflare Turn completes with a status other than 201 — e.g. 401/403 from an invalid or expired Cloudflare API token, 400 from a bad realm/key id, or 4xx/5xx from Cloudflare-side failures.","commonSituations":"Rotated or misconfigured Cloudflare API token in configuration, wrong turn realm or key id, Cloudflare outage or rate limiting, network path reaching a proxy that returns non-201 responses.","solutions":["Check the logged status code and response body to identify the exact Cloudflare rejection reason.","Verify the Cloudflare API token (cloudflare.credentials) is valid, has Turn permissions, and has not expired.","Confirm the configured key id and realm/tenant match the Cloudflare Turn setup.","Add retry with backoff for transient 5xx responses in the credentials fetch path."],"exampleFix":"// before: assumes success\nif (response.statusCode() != 201) { throw new IOException(\"Cloudflare Turn http failure : \" + response.statusCode()); }\n\n// after: fail fast with clearer config checks upstream\nif (cloudflareTurnToken == null || cloudflareTurnToken.isBlank()) {\n  throw new IllegalStateException(\"Cloudflare Turn API token not configured\");\n}\nif (response.statusCode() != 201) {\n  logger.error(\"Cloudflare Turn rejected request ({}): {}\", response.statusCode(), response.body());\n  throw new IOException(\"Cloudflare Turn http failure : \" + response.statusCode());\n}","handlingStrategy":"try-catch","validationCode":"// startup / pre-call config check\nif (cloudflareTurnToken == null || cloudflareTurnToken.isBlank() || keyId == null || realm == null) {\n  throw new IllegalStateException(\"Cloudflare Turn credentials/realm not configured\");\n}","typeGuard":null,"tryCatchPattern":"try {\n  TurnCredentials c = turnCredentialsManager.retrieveFromCloudflare(...).get();\n} catch (IOException e) {\n  logger.error(\"Cloudflare Turn credential fetch failed: {}\", e.getMessage());\n  // serve cached credentials or fail the endpoint with 503\n}","preventionTips":["Rotate and test the Cloudflare API token before expiry; alert on 401/403 status codes.","Add automated checks that the configured realm/key id return 201.","Cache last-known-good credentials so transient non-201 responses don't take the endpoint down."],"tags":["http","cloudflare","credentials","turn"],"backgroundTag":"http-non-2xx-response","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"}