signalapp/Signal-Server · error · IOException
failure request credentials from Cloudflare Turn
Error message
failure request credentials from Cloudflare Turn (code={}): {} What it means
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.
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.
Example fix
// before: assumes success
if (response.statusCode() != 201) { throw new IOException("Cloudflare Turn http failure : " + response.statusCode()); }
// after: fail fast with clearer config checks upstream
if (cloudflareTurnToken == null || cloudflareTurnToken.isBlank()) {
throw new IllegalStateException("Cloudflare Turn API token not configured");
}
if (response.statusCode() != 201) {
logger.error("Cloudflare Turn rejected request ({}): {}", response.statusCode(), response.body());
throw new IOException("Cloudflare Turn http failure : " + response.statusCode());
} Defensive patterns
Strategy: try-catch
Validate before calling
// startup / pre-call config check
if (cloudflareTurnToken == null || cloudflareTurnToken.isBlank() || keyId == null || realm == null) {
throw new IllegalStateException("Cloudflare Turn credentials/realm not configured");
} Try / catch
try {
TurnCredentials c = turnCredentialsManager.retrieveFromCloudflare(...).get();
} catch (IOException e) {
logger.error("Cloudflare Turn credential fetch failed: {}", e.getMessage());
// serve cached credentials or fail the endpoint with 503
} Prevention
- 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.
When it happens
Trigger: 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.
Common situations: 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.
Related errors
- Bad decoded value:
- Badly-formatted credentials:
- Username or password were blank
- Empty body not allowed
- Got a non-200 reply from source URI:
AI-assisted analysis of signalapp/Signal-Server@100ab61c82 (2026-09-09).
Data as JSON: /api/errors/65446056d2472167.
Report an issue: GitHub.
Appendix: source
Thrown at service/src/main/java/org/whispersystems/textsecuregcm/auth/CloudflareTurnCredentialsManager.java:143
case Inet6Address i6 -> "[" + i6.getHostAddress() + "]";
default -> i.getHostAddress();
})
.flatMap(i -> turnUrlsWithIps.stream().map(u -> u.formatted(i)))
.toList();
} catch (Exception e) {
throw new IOException(e);
}
final HttpResponse<String> response;
try {
response = cloudflareTurnClient.sendAsync(getCredentialsRequest, HttpResponse.BodyHandlers.ofString()).join();
} catch (CompletionException e) {
logger.warn("failed to make http request to Cloudflare Turn: {}", e.getMessage());
throw new IOException(ExceptionUtils.unwrap(e));
}
if (response.statusCode() != Response.Status.CREATED.getStatusCode()) {
logger.warn("failure request credentials from Cloudflare Turn (code={}): {}", response.statusCode(), response);
throw new IOException("Cloudflare Turn http failure : " + response.statusCode());
}
final CloudflareTurnResponse cloudflareTurnResponse = SystemMapper.jsonMapper()
.readValue(response.body(), CloudflareTurnResponse.class);
return new TurnToken(
cloudflareTurnResponse.iceServers().username(),
cloudflareTurnResponse.iceServers().credential(),
clientCredentialTtl.toSeconds(),
turnUrls,
cloudflareTurnComposedUrls,
turnHostname
);
}
}
View on GitHub (pinned to 100ab61c82)