{"record":{"id":"60be1d3c9602bed7","repo":"SonarSource/sonarqube","slug":"no-html-link-found-for-bitbucket-cloud-repository","errorCode":null,"errorMessage":"No html link found for Bitbucket Cloud repository '%s'","messagePattern":"No html link found for Bitbucket Cloud repository '(.+?)'","errorType":"exception","errorClass":"IllegalStateException","httpStatus":500,"severity":"error","filePath":"server/sonar-webserver-webapi/src/main/java/org/sonar/server/almsettings/ws/ProjectBindingsServiceServerImpl.java","lineNumber":332,"sourceCode":"    String url = repository.getSelfHref();\n    if (url == null) {\n      throw new IllegalStateException(format(\"No self link found for Bitbucket Server repository '%s'\", sanitizeForLog(almSlug)));\n    }\n    return new LiveResolution(url, String.valueOf(repository.getId()));\n  }\n\n  private LiveResolution resolveBitbucketCloud(AlmSettingDto almSetting, ProjectAlmSettingDto projectAlmSetting, Map<String, String> tokenCache) {\n    String clientId = requireNonNull(almSetting.getClientId(), CLIENT_ID_CANNOT_BE_NULL);\n    String clientSecret = requireNonNull(almSetting.getDecryptedClientSecret(encryption), CLIENT_SECRET_CANNOT_BE_NULL);\n    String workspace = requireNonNull(almSetting.getAppId(), WORKSPACE_CANNOT_BE_NULL);\n    String almRepo = requireNonNull(projectAlmSetting.getAlmRepo(), ALM_REPO_CANNOT_BE_NULL);\n    // The OAuth token is workspace-wide, not per-repository: reuse it across every binding of this ALM setting\n    // resolved within the same request instead of minting a fresh one per binding.\n    String accessToken = tokenCache.computeIfAbsent(almSetting.getUuid(), uuid -> bitbucketCloudRestClient.createAccessToken(clientId, clientSecret));\n    org.sonar.alm.client.bitbucket.bitbucketcloud.Repository repository = bitbucketCloudRestClient.getRepoWithAccessToken(accessToken, workspace, almRepo);\n    String url = repository.getHtmlHref();\n    if (url == null) {\n      throw new IllegalStateException(format(\"No html link found for Bitbucket Cloud repository '%s'\", sanitizeForLog(almRepo)));\n    }\n    String repoId = repository.getUuid();\n    if (repoId == null) {\n      throw new IllegalStateException(format(\"No uuid found for Bitbucket Cloud repository '%s'\", sanitizeForLog(almRepo)));\n    }\n    return new LiveResolution(url, repoId);\n  }\n\n  /**\n   * GitHub's installation-token \"repositories\" scoping parameter expects the bare repository name\n   * (no {@code owner/} prefix), unlike {@code almRepo} which is stored as {@code owner/repo}.\n   */\n  private static String bareRepositoryName(String almRepo) {\n    int lastSlash = almRepo.lastIndexOf('/');\n    return lastSlash < 0 ? almRepo : almRepo.substring(lastSlash + 1);\n  }\n\n  /**","sourceCodeStart":314,"sourceCodeEnd":350,"githubUrl":"https://github.com/SonarSource/sonarqube/blob/184c821202192afc1c599fc912d0889b69fffa53/server/sonar-webserver-webapi/src/main/java/org/sonar/server/almsettings/ws/ProjectBindingsServiceServerImpl.java#L314-L350","documentation":"SonarQube throws this when resolving a Bitbucket Cloud project binding and the repository payload returned by the Bitbucket Cloud API has no 'html' link. The html href is the canonical web URL required to store in the binding. It signals the API response lacks the expected links.html.href field.","triggerScenarios":"resolveLive -> resolveBitbucketCloud calls bitbucketCloudRestClient.getRepoWithAccessToken(accessToken, workspace, almRepo) and the returned Repository's getHtmlHref() is null — e.g. wrong workspace/repo slug, or a response shape without links.html.","commonSituations":"Typo in workspace or repository name; repo moved/renamed; API returning 4xx wrapped as empty links; custom API gateway stripping links fields.","solutions":["Confirm the workspace and almRepo values resolve to an existing Bitbucket Cloud repository","Check GET /2.0/repositories/{workspace}/{repo} returns links.html.href","Regenerate the OAuth client credentials/refresh the access token if responses are degraded","Re-bind the project with corrected workspace/repo settings"],"exampleFix":"// before\nworkspace: \"acme\", almRepo: \"rep\"  // typo\n// after\nworkspace: \"acme\", almRepo: \"repo\"","handlingStrategy":"validation","validationCode":"const res = await fetch(`https://api.bitbucket.org/2.0/repositories/${workspace}/${repo}`, { headers: { Authorization: `Bearer ${token}` } });\nconst data = await res.json();\nif (!data.links?.html?.href) {\n  throw new Error(`Repo ${workspace}/${repo} has no html link; check workspace/slug and token scope`);\n}","typeGuard":"function hasHtmlHref(repo) {\n  return typeof repo?.links?.html?.href === 'string' && repo.links.html.href.length > 0;\n}","tryCatchPattern":null,"preventionTips":["Verify workspace and repository slugs are exact before binding","Use an OAuth token with repository read scope for the correct workspace","Confirm GET /2.0/repositories/{workspace}/{repo} returns links.html.href in your environment (watch proxies)","Re-check bindings after repo renames or workspace migrations"],"tags":["bitbucket-cloud","alm-binding","rest-client","missing-link"],"backgroundTag":"unexpected-api-response-shape","analyzedSha":"184c821202192afc1c599fc912d0889b69fffa53","analyzedAt":"2026-09-09T12:23:51.573Z","contentChangedAt":"2026-09-09T12:23:51.573Z","schemaVersion":2},"datasetVersion":"2026-09-17T15:17:12.973Z"}