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

  1. Check the logged status code and response body to identify the exact Cloudflare rejection reason.
  2. Verify the Cloudflare API token (cloudflare.credentials) is valid, has Turn permissions, and has not expired.
  3. Confirm the configured key id and realm/tenant match the Cloudflare Turn setup.
  4. 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

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


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)