SonarSource/sonarqube · error · BitbucketCloudException

Error returned by Bitbucket Cloud: %s [HTTP %s] | Unable to

Error message

Error returned by Bitbucket Cloud: %s [HTTP %s] | Unable to contact Bitbucket Cloud servers [HTTP %s]

What it means

handleError builds the failure message for any non-2xx Bitbucket Cloud API response and throws BitbucketCloudException(errorMessage, statusCode). If the response body was JSON with a parseable 'error.message', the message is 'Error returned by Bitbucket Cloud: <parsed msg> [HTTP <code>]'; otherwise it falls back to 'Unable to contact Bitbucket Cloud servers [HTTP <code>]'. The status code (401, 403, 404, 429, 5xx...) is the real signal about what went wrong.

Source

Thrown at server/sonar-alm-client/src/main/java/org/sonar/alm/client/bitbucket/bitbucketcloud/BitbucketCloudRestClient.java:247

      }
      return handler.apply(response);
    } catch (IOException e) {
      LOG.info(ERROR_BBC_SERVERS + ": {}", e.getMessage());
      throw new IllegalStateException(ERROR_BBC_SERVERS, e);
    }
  }

  private static void handleError(Response response) throws IOException {
    ErrorDetails error = getError(response.body(), response.message());
    int statusCode = response.code();
    LOG.atInfo().log(() -> String.format(BBC_FAIL_WITH_RESPONSE, response.request().url(), statusCode, error.body));
    String errorMessage;
    if (error.parsedErrorMsg != null) {
      errorMessage = ERROR_BBC_SERVERS + ": " + error.parsedErrorMsg + " [HTTP " + statusCode + "]";
    } else {
      errorMessage = UNABLE_TO_CONTACT_BBC_SERVERS + " [HTTP " + statusCode + "]";
    }
    throw new BitbucketCloudException(errorMessage, statusCode);
  }

  private static ErrorDetails getError(@Nullable ResponseBody body, @Nullable String fallbackMessage) throws IOException {
    return getErrorDetails(body, fallbackMessage, s -> {
      Error gsonError = buildGson().fromJson(s, Error.class);
      if (gsonError != null && gsonError.errorMsg != null && gsonError.errorMsg.message != null) {
        return gsonError.errorMsg.message;
      }
      return null;
    });
  }

  private static ErrorDetails getTokenError(@Nullable ResponseBody body, @Nullable String fallbackMessage) throws IOException {
    if (body == null) {
      return new ErrorDetails(fallbackMessage, null);
    }
    String bodyStr = body.string();
    if (bodyStr.isBlank()) {

View on GitHub (pinned to 184c821202)

Solutions

  1. Parse the '[HTTP <code>]' suffix from the message and branch on it — the code is the authoritative cause.
  2. 401/403: regenerate the access token or API token and ensure the OAuth consumer/token has the required scopes (repository read, pull request read).
  3. 404: verify the workspace and repository slugs passed to getRepo/searchRepos match Bitbucket Cloud exactly.
  4. 429: add/exponential backoff in the caller and reduce polling frequency; honor the Retry-After header.
  5. 5xx or HTML-body responses: retry with backoff and check the Bitbucket Cloud status page; treat it as transient.

Example fix

// before: treating every BitbucketCloudException the same
try { client.getRepo(cred, ws, slug); } catch (BitbucketCloudException e) { retry(); }
// after: branch on the embedded status code
try { client.getRepo(cred, ws, slug); }
catch (BitbucketCloudException e) {
  if (e.getHttpCode() == 404) { throw new IllegalArgumentException("Unknown repo " + slug); }
  if (e.getHttpCode() == 429 || e.getHttpCode() >= 500) { backoffAndRetry(); }
  else { throw new IllegalStateException("Auth/permission problem: " + e.getMessage(), e); }
}
Defensive patterns

Strategy: type-guard

Validate before calling

// Before calling the API, validate inputs that drive the URL and auth:
static void precheck(String workspace, String slug, String token) {
  if (workspace == null || !workspace.matches("[A-Za-z0-9][A-Za-z0-9_.-]*"))
    throw new IllegalArgumentException("Invalid workspace slug: prevents 404s");
  if (slug != null && !slug.matches("[A-Za-z0-9_.-]+"))
    throw new IllegalArgumentException("Invalid repo slug: prevents 404s");
  if (token == null || token.isBlank())
    throw new IllegalArgumentException("Blank token: prevents 401s");
}

Type guard

// Narrow BitbucketCloudException by its embedded HTTP status before reacting:
static boolean isAuthError(BitbucketCloudException e) {
  return e.getHttpCode() == 401 || e.getHttpCode() == 403;
}
static boolean isNotFound(BitbucketCloudException e) { return e.getHttpCode() == 404; }
static boolean isRateLimited(BitbucketCloudException e) { return e.getHttpCode() == 429; }
static boolean isTransient(BitbucketCloudException e) {
  return e.getHttpCode() == 429 || e.getHttpCode() >= 500;
}

Try / catch

try {
  return restClient.getRepo(cred, workspace, slug);
} catch (BitbucketCloudException e) {
  int code = e.getHttpCode();
  if (code == 401 || code == 403) {
    throw new ConfigurationException("Bitbucket token invalid or missing scopes (HTTP " + code + ")", e);
  } else if (code == 404) {
    throw new IllegalArgumentException("Workspace/repo not found: '" + workspace + "/" + slug + "'");
  } else if (code == 429 || code >= 500) {
    throw new TransientApiException("Bitbucket Cloud temporarily failing (HTTP " + code + "), retry with backoff", e);
  }
  throw e; // includes the '[HTTP n]' suffix and any parsed Bitbucket message
}

Prevention

When it happens

Trigger: Thrown by handleError, invoked from doCall (callers: createAccessToken, doGet, doGetWithApiToken) whenever response.isSuccessful() is false on calls to api.bitbucket.org/2.0/... : 401 invalid credentials, 403 missing permission, 404 unknown workspace/repo, 429 rate limited, 5xx server errors; or when the error body is not JSON / lacks error.errorMsg.message so no parsed message is available.

Common situations: Expired or revoked access/API token (401); OAuth token lacking the 'pullrequest' scope; querying a workspace slug that does not exist or is misspelled (404); hitting Bitbucket Cloud rate limits on polling integrations (429); transient 502/503 during Bitbucket incidents where the body is HTML so no parsed message is available; firewall returning a captive-portal HTML page.

Related errors


AI-assisted analysis of SonarSource/sonarqube@184c821202 (2026-09-09). Data as JSON: /api/errors/eabc988eafe6b60e. Report an issue: GitHub.