From cbd40f3e7cd16e7682b1cb80b27f4b803a3a58f9 Mon Sep 17 00:00:00 2001 From: Ubuntu Date: Mon, 13 Apr 2026 17:19:42 +0000 Subject: [PATCH 1/4] Fix error type for non-JSON error responses When the server returns a plain-text error (e.g. "Invalid Token" with HTTP 403), parseUnknownError set the error code to "UNKNOWN", which matched ErrorMapper's errorCodeMapping and short-circuited the status code mapping. This caused all non-JSON errors to produce Unknown instead of the correct typed exception (PermissionDenied, Unauthenticated, etc.). The root cause was that parseUnknownError assumed response.getStatus() returned "403 Forbidden" but it only returns the reason phrase ("Forbidden"). The split-on-space logic always fell into the default "UNKNOWN" branch. The fix removes the error code derivation entirely, leaving errorCode null so AbstractErrorMapper falls through to the status code mapping. The error message now uses the raw response body instead of appending Jackson parse exception details. Co-authored-by: Isaac --- .../databricks/sdk/core/error/ApiErrors.java | 16 +---- .../sdk/core/error/PlainTextErrorTest.java | 65 +++++++++++++++++++ 2 files changed, 68 insertions(+), 13 deletions(-) create mode 100644 databricks-sdk-java/src/test/java/com/databricks/sdk/core/error/PlainTextErrorTest.java diff --git a/databricks-sdk-java/src/main/java/com/databricks/sdk/core/error/ApiErrors.java b/databricks-sdk-java/src/main/java/com/databricks/sdk/core/error/ApiErrors.java index bbef61d8c..c67ea622b 100644 --- a/databricks-sdk-java/src/main/java/com/databricks/sdk/core/error/ApiErrors.java +++ b/databricks-sdk-java/src/main/java/com/databricks/sdk/core/error/ApiErrors.java @@ -116,27 +116,17 @@ private static Optional 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)); } } - 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()); - } - Matcher messageMatcher = HTML_ERROR_REGEX.matcher(body); if (messageMatcher.find()) { errorBody.setMessage(messageMatcher.group(1).replaceAll("^[ .]+|[ .]+$", "")); } else { - errorBody.setMessage( - String.format( - "Response from server (%s) %s: %s", response.getStatus(), body, err.getMessage())); + errorBody.setMessage(body); } return errorBody; } diff --git a/databricks-sdk-java/src/test/java/com/databricks/sdk/core/error/PlainTextErrorTest.java b/databricks-sdk-java/src/test/java/com/databricks/sdk/core/error/PlainTextErrorTest.java new file mode 100644 index 000000000..97aebce9c --- /dev/null +++ b/databricks-sdk-java/src/test/java/com/databricks/sdk/core/error/PlainTextErrorTest.java @@ -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 = "
some error message
"; + 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); + } +} From 0239758eb48a4569b806cba2545244df703b807a Mon Sep 17 00:00:00 2001 From: Ubuntu Date: Mon, 13 Apr 2026 17:21:19 +0000 Subject: [PATCH 2/4] Add changelog entry for plain-text error handling fix Co-authored-by: Isaac --- NEXT_CHANGELOG.md | 1 + 1 file changed, 1 insertion(+) diff --git a/NEXT_CHANGELOG.md b/NEXT_CHANGELOG.md index 0bd9e224b..d73cb7735 100755 --- a/NEXT_CHANGELOG.md +++ b/NEXT_CHANGELOG.md @@ -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/` 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. * 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. ### Security Vulnerabilities From e9b6d0bad1321b24665d9f008bf4549ad1139db0 Mon Sep 17 00:00:00 2001 From: Ubuntu Date: Tue, 14 Apr 2026 10:54:13 +0000 Subject: [PATCH 3/4] Set non-null error code for non-JSON error responses Avoid NPE in downstream .equals() and Pattern.matcher() calls when the error response cannot be parsed as JSON. Co-authored-by: Isaac --- .../src/main/java/com/databricks/sdk/core/error/ApiErrors.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/databricks-sdk-java/src/main/java/com/databricks/sdk/core/error/ApiErrors.java b/databricks-sdk-java/src/main/java/com/databricks/sdk/core/error/ApiErrors.java index c67ea622b..e642fd12a 100644 --- a/databricks-sdk-java/src/main/java/com/databricks/sdk/core/error/ApiErrors.java +++ b/databricks-sdk-java/src/main/java/com/databricks/sdk/core/error/ApiErrors.java @@ -122,9 +122,9 @@ private static Optional parseApiError(Response response) { private static ApiErrorBody parseUnknownError(String body) { ApiErrorBody errorBody = new ApiErrorBody(); + errorBody.setErrorCode(""); // non-null to avoid NPE Matcher messageMatcher = HTML_ERROR_REGEX.matcher(body); if (messageMatcher.find()) { - errorBody.setMessage(messageMatcher.group(1).replaceAll("^[ .]+|[ .]+$", "")); } else { errorBody.setMessage(body); } From 8e6cbe93b48c3c49fc123a11ed8a1e1a81c3b262 Mon Sep 17 00:00:00 2001 From: Ubuntu Date: Tue, 14 Apr 2026 14:59:22 +0000 Subject: [PATCH 4/4] Fix missing message extraction for HTML error responses The
 content extraction was accidentally removed, causing
null error messages for HTML error responses.

Co-authored-by: Isaac
---
 .../src/main/java/com/databricks/sdk/core/error/ApiErrors.java   | 1 +
 1 file changed, 1 insertion(+)

diff --git a/databricks-sdk-java/src/main/java/com/databricks/sdk/core/error/ApiErrors.java b/databricks-sdk-java/src/main/java/com/databricks/sdk/core/error/ApiErrors.java
index e642fd12a..9a1c60b65 100644
--- a/databricks-sdk-java/src/main/java/com/databricks/sdk/core/error/ApiErrors.java
+++ b/databricks-sdk-java/src/main/java/com/databricks/sdk/core/error/ApiErrors.java
@@ -125,6 +125,7 @@ private static ApiErrorBody parseUnknownError(String body) {
     errorBody.setErrorCode(""); // non-null to avoid NPE
     Matcher messageMatcher = HTML_ERROR_REGEX.matcher(body);
     if (messageMatcher.find()) {
+      errorBody.setMessage(messageMatcher.group(1));
     } else {
       errorBody.setMessage(body);
     }