Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions NEXT_CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@
* Added automatic detection of AI coding agents (Antigravity, Claude Code, Cline, Codex, Copilot CLI, Cursor, Gemini CLI, OpenCode) in the user-agent string. The SDK now appends `agent/<name>` to HTTP request headers when running inside a known AI agent environment.

### Bug Fixes
* Fixed non-JSON error responses (e.g. plain-text "Invalid Token" with HTTP 403) producing `Unknown` instead of the correct typed exception (`PermissionDenied`, `Unauthenticated`, etc.). The error message no longer contains Jackson deserialization internals.
* Added `X-Databricks-Org-Id` header to deprecated workspace SCIM APIs (Groups, ServicePrincipals, Users) for SPOG host compatibility.
* Fixed Databricks CLI authentication to detect when the cached token's scopes don't match the SDK's configured scopes. Previously, a scope mismatch was silently ignored, causing requests to use wrong permissions. The SDK now raises an error with instructions to re-authenticate.

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -116,27 +116,18 @@ private static Optional<ApiErrorBody> parseApiError(Response response) {
try {
return Optional.of(MAPPER.readValue(body, ApiErrorBody.class));
} catch (IOException e) {
return Optional.of(parseUnknownError(response, body, e));
return Optional.of(parseUnknownError(body));
Comment thread
renaudhartert-db marked this conversation as resolved.
}
}

private static ApiErrorBody parseUnknownError(Response response, String body, IOException err) {
private static ApiErrorBody parseUnknownError(String body) {
ApiErrorBody errorBody = new ApiErrorBody();
String[] statusParts = response.getStatus().split(" ", 2);
if (statusParts.length < 2) {
errorBody.setErrorCode("UNKNOWN");
} else {
String errorCode = statusParts[1].replaceAll("^[ .]+|[ .]+$", "");
errorBody.setErrorCode(errorCode.replaceAll(" ", "_").toUpperCase());
}

errorBody.setErrorCode(""); // non-null to avoid NPE
Matcher messageMatcher = HTML_ERROR_REGEX.matcher(body);
if (messageMatcher.find()) {
errorBody.setMessage(messageMatcher.group(1).replaceAll("^[ .]+|[ .]+$", ""));
Comment thread
renaudhartert-db marked this conversation as resolved.
errorBody.setMessage(messageMatcher.group(1));
} else {
errorBody.setMessage(
String.format(
"Response from server (%s) %s: %s", response.getStatus(), body, err.getMessage()));
errorBody.setMessage(body);
}
return errorBody;
}
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,65 @@
package com.databricks.sdk.core.error;

import static org.junit.jupiter.api.Assertions.*;

import com.databricks.sdk.core.DatabricksError;
import com.databricks.sdk.core.error.platform.*;
import com.databricks.sdk.core.http.Request;
import com.databricks.sdk.core.http.Response;
import java.util.Collections;
import org.junit.jupiter.api.Test;

class PlainTextErrorTest {

@Test
void plainTextForbiddenReturnsPermissionDenied() {
DatabricksError error = getError(403, "Forbidden", "Invalid Token");
assertInstanceOf(PermissionDenied.class, error);
assertEquals("Invalid Token", error.getMessage());
}

@Test
void plainTextUnauthorizedReturnsUnauthenticated() {
DatabricksError error = getError(401, "Unauthorized", "Bad credentials");
assertInstanceOf(Unauthenticated.class, error);
assertEquals("Bad credentials", error.getMessage());
}

@Test
void plainTextNotFoundReturnsNotFound() {
DatabricksError error = getError(404, "Not Found", "no such endpoint");
assertInstanceOf(NotFound.class, error);
assertEquals("no such endpoint", error.getMessage());
}

@Test
void htmlErrorExtractsPreContent() {
String html = "<html><body><pre>some error message</pre></body></html>";
DatabricksError error = getError(403, "Forbidden", html);
assertInstanceOf(PermissionDenied.class, error);
assertEquals("some error message", error.getMessage());
}

@Test
void emptyBodyFallsBackToStatusCode() {
Request request = new Request("GET", "https://example.com/api/2.0/clusters/get");
Response response = new Response(request, 403, "Forbidden", Collections.emptyMap(), "");
DatabricksError error = ApiErrors.getDatabricksError(response);
assertInstanceOf(PermissionDenied.class, error);
}

@Test
void nullBodyFallsBackToStatusCode() {
Request request = new Request("GET", "https://example.com/api/2.0/clusters/get");
Response response =
new Response(request, 403, "Forbidden", Collections.emptyMap(), (String) null);
DatabricksError error = ApiErrors.getDatabricksError(response);
assertInstanceOf(PermissionDenied.class, error);
}

private static DatabricksError getError(int statusCode, String status, String body) {
Request request = new Request("GET", "https://example.com/api/2.0/clusters/get");
Response response = new Response(request, statusCode, status, Collections.emptyMap(), body);
return ApiErrors.getDatabricksError(response);
}
}
Loading