Skip to content

feat(sdk): structured error responses with typed error codes#27

Open
MDzaja wants to merge 1 commit into
mainfrom
clients/pr-4849-errors-daemon
Open

feat(sdk): structured error responses with typed error codes#27
MDzaja wants to merge 1 commit into
mainfrom
clients/pr-4849-errors-daemon

Conversation

@MDzaja

@MDzaja MDzaja commented Jul 1, 2026

Copy link
Copy Markdown
Collaborator

Original: daytonaio/daytona#4849


View with Codesmith Autofix with Codesmith
Need help on this PR? Tag /codesmith with what you need. Autofix is disabled.


Summary by cubic

Adds structured error envelopes and typed error codes across all SDKs and toolbox clients. Errors now include a machine-readable code and source, and map to consistent, language-idiomatic classes.

  • New Features

    • OpenAPI: adds ErrorResponse and DaemonErrorCode; all error responses reference it.
    • toolbox-api-client-go / toolbox-api-client-java: decode structured errors and expose them via generated ApiError.
    • sdk-go: single DaytonaError with code, source, headers, and status; package sentinels like sdkerrors.ErrNotFound; Client.handleAPIError delegates to structured conversion.
    • sdk-java: DaytonaException carries statusCode, code, source; many specific subclasses (e.g., DaytonaGitAuthFailedException, DaytonaServiceUnavailableException); 422 is now DaytonaUnprocessableEntityException; ExceptionMapper updated.
    • sdk-python: errors expose status_code, code, source; richer mapping for connection/timeout; filesystem download errors parse envelope; tests updated.
    • sdk-ruby: new Daytona::Sdk error hierarchy with code/source; wraps OpenAPI ApiError; file-download patch preserves error bodies for streaming; callers now raise consistent SDK errors.
    • sdk-typescript: DaytonaError gains code/source; more subclasses (e.g., DaytonaInternalServerError, DaytonaServiceUnavailableError); file download error details use code/source; axios mapping improved; tests updated.
  • Migration

    • Replace error.errorCode with error.code (TypeScript, Python). For file downloads, use errorDetails.code and errorDetails.source.
    • Java: catch DaytonaUnprocessableEntityException instead of DaytonaValidationException for 422.
    • Go: prefer errors.Is(err, sdkerrors.ErrNotFound) and other sentinels over struct type assertions.
    • Ruby: rescue Daytona::Sdk::Error subclasses; SDK methods now wrap toolbox/main API errors into these types.

Written for commit c72c6e1. Summary will update on new commits.

Review in cubic

Signed-off-by: MDzaja <mirkodzaja0@gmail.com>

@cubic-dev-ai cubic-dev-ai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

27 issues found across 152 files

Prompt for AI agents (unresolved issues)

Check if these issues are valid — if so, understand the root cause of each and fix them. If appropriate, use sub-agents to investigate and fix each issue separately.


<file name="sdk-python/tests/test_common.py">

<violation number="1" location="sdk-python/tests/test_common.py:233">
P2: Removes coverage of the `error_code` backwards-compat fallback that still exists in the parser (filesystem.py:168-169). The old snake_case test payload used `error_code`, which exercised the retro-compat path for older runner builds. Without a dedicated test, this code is now dead-code-grey: it will silently bit-rot without detection.</violation>
</file>

<file name="toolbox-api-client-java/src/main/java/io/daytona/toolbox/client/api/FileSystemApi.java">

<violation number="1" location="toolbox-api-client-java/src/main/java/io/daytona/toolbox/client/api/FileSystemApi.java:30">
P3: Unused `ErrorResponse` import adds dead code noise. Remove it to keep generated client clean and avoid lint/checkstyle failures.</violation>
</file>

<file name="sdk-typescript/src/utils/FileTransfer.ts">

<violation number="1" location="sdk-typescript/src/utils/FileTransfer.ts:37">
P2: `error_code` fallback was removed, so structured code parsing regresses for snake_case responses. This can downgrade typed download errors to generic status-only errors.</violation>
</file>

<file name="sdk-java/src/main/java/io/daytona/sdk/exception/DaytonaException.java">

<violation number="1" location="sdk-java/src/main/java/io/daytona/sdk/exception/DaytonaException.java:47">
P2: Headers are wrapped, not copied, so external map mutations can alter exception headers after creation. Snapshot the map during construction.</violation>
</file>

<file name="toolbox-api-client-java/src/main/java/io/daytona/toolbox/client/model/ErrorResponse.java">

<violation number="1" location="toolbox-api-client-java/src/main/java/io/daytona/toolbox/client/model/ErrorResponse.java:343">
P2: Validation assumes object input and can throw unchecked exceptions for non-object JSON. Add an explicit isJsonObject guard before required-field access.</violation>

<violation number="2" location="toolbox-api-client-java/src/main/java/io/daytona/toolbox/client/model/ErrorResponse.java:403">
P2: Additional-property serialization crashes for non-object values (e.g., enum or null). Serialize the JsonElement directly instead of forcing object casting.</violation>
</file>

<file name="sdk-java/src/main/java/io/daytona/sdk/ExceptionMapper.java">

<violation number="1" location="sdk-java/src/main/java/io/daytona/sdk/ExceptionMapper.java:148">
P2: Status/code mapped exceptions discard response headers. Callers of `getHeaders()` on common mapped errors (e.g., 429) will not receive server metadata like retry hints.</violation>

<violation number="2" location="sdk-java/src/main/java/io/daytona/sdk/ExceptionMapper.java:169">
P2: Transport timeouts no longer map to DaytonaTimeoutException, which is a breaking behavioral change for existing timeout catch blocks.</violation>
</file>

<file name="toolbox-api-client-python-async/daytona_toolbox_api_client_async/api/git_api.py">

<violation number="1" location="toolbox-api-client-python-async/daytona_toolbox_api_client_async/api/git_api.py:106">
P3: Added error mappings in `without_preload_content` methods are unused dead code. These methods return raw response without deserializing, so the new `_response_types_map` entries have no effect.</violation>
</file>

<file name="sdk-java/src/main/java/io/daytona/sdk/exception/DaytonaTimeoutException.java">

<violation number="1" location="sdk-java/src/main/java/io/daytona/sdk/exception/DaytonaTimeoutException.java:12">
P3: New field comment is incorrect: not every DaytonaTimeoutException instance carries HTTP 408.</violation>

<violation number="2" location="sdk-java/src/main/java/io/daytona/sdk/exception/DaytonaTimeoutException.java:39">
P1: Timeout constructor now hardcodes 408, so mapped HTTP 504 errors are reported with the wrong status code.</violation>
</file>

<file name="sdk-ruby/lib/daytona/git.rb">

<violation number="1" location="sdk-ruby/lib/daytona/git.rb:321">
P2: This loses ServerError classification for unlisted 5xx Git API responses. A 501/505/520 toolbox response now raises base Daytona::Sdk::Error, so callers rescuing ServerError miss server-side failures.</violation>
</file>

<file name="sdk-ruby/lib/daytona/file_system.rb">

<violation number="1" location="sdk-ruby/lib/daytona/file_system.rb:154">
P2: `download_file` no longer wraps local filesystem errors. Failures writing `local_path` now escape as raw `Errno`/`IOError` instead of SDK errors.</violation>

<violation number="2" location="sdk-ruby/lib/daytona/file_system.rb:238">
P2: `upload_file` now misses non-API upload failures from local IO handling. This breaks consistent SDK error typing for upload failures before the API call completes.</violation>
</file>

<file name="toolbox-api-client-ruby/lib/daytona_toolbox_api_client/models/error_response.rb">

<violation number="1" location="toolbox-api-client-ruby/lib/daytona_toolbox_api_client/models/error_response.rb:23">
P2: `method` accessor shadows `Object#method`, breaking standard Ruby reflection on `ErrorResponse` instances. Use a non-reserved Ruby attribute name (e.g. `http_method`) mapped to JSON key `method`.</violation>
</file>

<file name="sdk-python/src/daytona/_utils/errors.py">

<violation number="1" location="sdk-python/src/daytona/_utils/errors.py:81">
P2: OpenAPI error parsing dropped backward-compatible `error_code`/`error` keys. Older structured responses will no longer populate `DaytonaError.code` or code-based subclass mapping.</violation>
</file>

<file name="toolbox-api-client-python/daytona_toolbox_api_client/api/port_api.py">

<violation number="1" location="toolbox-api-client-python/daytona_toolbox_api_client/api/port_api.py:341">
P3: This added 400 mapping is dead code in without_preload_content; that variant returns the raw response and never deserializes with _response_types_map.</violation>
</file>

<file name="toolbox-api-client-go/api_lsp.go">

<violation number="1" location="toolbox-api-client-go/api_lsp.go:225">
P3: Inconsistent indentation in generated code: `formatErrorMessage`/`newErr.model` lines use 5 tabs instead of 3. Since this is generated code, the OpenAPI generator template should be fixed upstream.</violation>
</file>

<file name="sdk-python/src/daytona/common/errors.py">

<violation number="1" location="sdk-python/src/daytona/common/errors.py:159">
P2: `DaytonaBadRequestError` docstring claims it's for "HTTP 400" but the deprecated alias `DaytonaValidationError` is used for client-side validation that never involves an HTTP request. The class docstring and example are misleading for the aliased usage. Consider either keeping a separate `DaytonaValidationError` class for client-side errors or updating the docstring to cover both use-cases.</violation>

<violation number="2" location="sdk-python/src/daytona/common/errors.py:195">
P2: `DaytonaConnectionTimeoutError` inherits only from `DaytonaConnectionError`, not `DaytonaTimeoutError`. Users writing `except DaytonaTimeoutError` to catch all timeouts will miss transport-level timeouts. Consider multiple inheritance (`class DaytonaConnectionTimeoutError(DaytonaConnectionError, DaytonaTimeoutError)`) for consistency with `DaytonaProcessExecutionTimeoutError`.</violation>
</file>

<file name="sdk-go/pkg/errors/errors.go">

<violation number="1" location="sdk-go/pkg/errors/errors.go:102">
P3: Doc comment references nonexistent `SourceSDK` symbol, which is misleading for users.</violation>
</file>

<file name="sdk-ruby/lib/daytona/sdk/file_download_patch.rb">

<violation number="1" location="sdk-ruby/lib/daytona/sdk/file_download_patch.rb:81">
P1: Buffered file-download error data is stored in a shared instance variable, which is race-prone for concurrent requests and can attach the wrong error body to raised ApiError.</violation>

<violation number="2" location="sdk-ruby/lib/daytona/sdk/file_download_patch.rb:86">
P2: No guard against double-patching: calling `apply!` twice on the same class creates infinite recursion via `alias_method` re-aliasing the patched method.</violation>
</file>

<file name="sdk-typescript/src/errors/DaytonaError.ts">

<violation number="1" location="sdk-typescript/src/errors/DaytonaError.ts:66">
P2: `new DaytonaValidationError(msg).name` now returns `'DaytonaBadRequestError'` instead of `'DaytonaValidationError'` because the alias re-exports the class directly and `new.target.name` resolves to the actual constructor name. This is a subtle behavioral break for code relying on `error.name`.</violation>
</file>

<file name="sdk-ruby/lib/daytona/sdk/errors.rb">

<violation number="1" location="sdk-ruby/lib/daytona/sdk/errors.rb:271">
P3: Internal helper methods `parse_error_body`, `parsed_message`, `error_class_for`, and `string_or_nil` are publicly exposed on the `Daytona::Sdk` module but are implementation details. Mark them with `private_class_method` to avoid polluting the public API.</violation>
</file>

<file name="toolbox-api-client-java/src/main/java/io/daytona/toolbox/client/api/ProcessApi.java">

<violation number="1" location="toolbox-api-client-java/src/main/java/io/daytona/toolbox/client/api/ProcessApi.java:34">
P2: Unused `ErrorResponse` import introduces dead code and indicates typed error handling is not wired in this API class.</violation>
</file>

<file name="sdk-typescript/src/Daytona.ts">

<violation number="1" location="sdk-typescript/src/Daytona.ts:649">
P2: Timeout rethrow drops `source` metadata, losing structured error origin information.</violation>
</file>

Tip: instead of fixing issues one by one fix them all with cubic

Re-trigger cubic

}

public DaytonaTimeoutException(String message, Throwable cause, String code, String source) {
super(STATUS_CODE, message, cause, code, source);

@cubic-dev-ai cubic-dev-ai Bot Jul 1, 2026

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P1: Timeout constructor now hardcodes 408, so mapped HTTP 504 errors are reported with the wrong status code.

Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. At sdk-java/src/main/java/io/daytona/sdk/exception/DaytonaTimeoutException.java, line 39:

<comment>Timeout constructor now hardcodes 408, so mapped HTTP 504 errors are reported with the wrong status code.</comment>

<file context>
@@ -27,4 +30,12 @@ public DaytonaTimeoutException(String message, Throwable cause) {
+    }
+
+    public DaytonaTimeoutException(String message, Throwable cause, String code, String source) {
+        super(STATUS_CODE, message, cause, code, source);
+    }
 }
</file context>
Fix with cubic

)
block&.call(tempfile)
else
@_daytona_error_body = error_body unless error_body.empty?

@cubic-dev-ai cubic-dev-ai Bot Jul 1, 2026

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P1: Buffered file-download error data is stored in a shared instance variable, which is race-prone for concurrent requests and can attach the wrong error body to raised ApiError.

Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. At sdk-ruby/lib/daytona/sdk/file_download_patch.rb, line 81:

<comment>Buffered file-download error data is stored in a shared instance variable, which is race-prone for concurrent requests and can attach the wrong error body to raised ApiError.</comment>

<file context>
@@ -0,0 +1,122 @@
+                )
+                block&.call(tempfile)
+              else
+                @_daytona_error_body = error_body unless error_body.empty?
+              end
+            end
</file context>
Fix with cubic

# variant — payload uses the canonical key.
message, details = parse_file_download_error_payload(
b'{"message":"missing","status_code":404,"error_code":"NOT_FOUND"}',
b'{"message":"missing","status_code":404,"code":"NOT_FOUND"}',

@cubic-dev-ai cubic-dev-ai Bot Jul 1, 2026

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2: Removes coverage of the error_code backwards-compat fallback that still exists in the parser (filesystem.py:168-169). The old snake_case test payload used error_code, which exercised the retro-compat path for older runner builds. Without a dedicated test, this code is now dead-code-grey: it will silently bit-rot without detection.

Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. At sdk-python/tests/test_common.py, line 233:

<comment>Removes coverage of the `error_code` backwards-compat fallback that still exists in the parser (filesystem.py:168-169). The old snake_case test payload used `error_code`, which exercised the retro-compat path for older runner builds. Without a dedicated test, this code is now dead-code-grey: it will silently bit-rot without detection.</comment>

<file context>
@@ -226,19 +226,22 @@ def test_create_file_download_error_requires_error_message(self):
+        # variant — payload uses the canonical key.
         message, details = parse_file_download_error_payload(
-            b'{"message":"missing","status_code":404,"error_code":"NOT_FOUND"}',
+            b'{"message":"missing","status_code":404,"code":"NOT_FOUND"}',
             "application/json",
         )
</file context>
Fix with cubic

const structuredMessage = payloadObject.message
const statusCode = payloadObject.statusCode ?? payloadObject.status_code
const errorCode = payloadObject.code ?? payloadObject.error_code
const code = payloadObject.code

@cubic-dev-ai cubic-dev-ai Bot Jul 1, 2026

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2: error_code fallback was removed, so structured code parsing regresses for snake_case responses. This can downgrade typed download errors to generic status-only errors.

Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. At sdk-typescript/src/utils/FileTransfer.ts, line 37:

<comment>`error_code` fallback was removed, so structured code parsing regresses for snake_case responses. This can downgrade typed download errors to generic status-only errors.</comment>

<file context>
@@ -34,7 +34,8 @@ function parseDownloadErrorPart(data: Uint8Array, contentType?: string): Downloa
     const structuredMessage = payloadObject.message
     const statusCode = payloadObject.statusCode ?? payloadObject.status_code
-    const errorCode = payloadObject.code ?? payloadObject.error_code
+    const code = payloadObject.code
+    const source = payloadObject.source
 
</file context>
Suggested change
const code = payloadObject.code
const code = payloadObject.code ?? payloadObject.error_code
Fix with cubic

Throwable cause) {
super(message, cause);
this.statusCode = statusCode;
this.headers = headers != null ? Collections.unmodifiableMap(headers) : Collections.emptyMap();

@cubic-dev-ai cubic-dev-ai Bot Jul 1, 2026

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2: Headers are wrapped, not copied, so external map mutations can alter exception headers after creation. Snapshot the map during construction.

Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. At sdk-java/src/main/java/io/daytona/sdk/exception/DaytonaException.java, line 47:

<comment>Headers are wrapped, not copied, so external map mutations can alter exception headers after creation. Snapshot the map during construction.</comment>

<file context>
@@ -25,18 +25,37 @@
+            Throwable cause) {
+        super(message, cause);
+        this.statusCode = statusCode;
+        this.headers = headers != null ? Collections.unmodifiableMap(headers) : Collections.emptyMap();
+        this.code = code;
+        this.source = source;
</file context>
Suggested change
this.headers = headers != null ? Collections.unmodifiableMap(headers) : Collections.emptyMap();
this.headers = headers != null ? Collections.unmodifiableMap(new java.util.HashMap<>(headers)) : Collections.emptyMap();
Fix with cubic

* <p>This exception is generated client-side and is not tied to a single HTTP status code.
*/
public class DaytonaTimeoutException extends DaytonaException {
/** HTTP status code carried by every instance of this class. */

@cubic-dev-ai cubic-dev-ai Bot Jul 1, 2026

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P3: New field comment is incorrect: not every DaytonaTimeoutException instance carries HTTP 408.

Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. At sdk-java/src/main/java/io/daytona/sdk/exception/DaytonaTimeoutException.java, line 12:

<comment>New field comment is incorrect: not every DaytonaTimeoutException instance carries HTTP 408.</comment>

<file context>
@@ -9,6 +9,9 @@
  * <p>This exception is generated client-side and is not tied to a single HTTP status code.
  */
 public class DaytonaTimeoutException extends DaytonaException {
+    /** HTTP status code carried by every instance of this class. */
+    public static final int STATUS_CODE = 408;
+
</file context>
Suggested change
/** HTTP status code carried by every instance of this class. */
/** Default HTTP status code for server-reported timeout responses. */
Fix with cubic

@@ -338,6 +338,7 @@ def is_port_in_use(

_response_types_map: Dict[str, Optional[str]] = {
'200': "IsPortInUseResponse",
'400': "ErrorResponse",

@cubic-dev-ai cubic-dev-ai Bot Jul 1, 2026

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P3: This added 400 mapping is dead code in without_preload_content; that variant returns the raw response and never deserializes with _response_types_map.

Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. At toolbox-api-client-python/daytona_toolbox_api_client/api/port_api.py, line 341:

<comment>This added 400 mapping is dead code in without_preload_content; that variant returns the raw response and never deserializes with _response_types_map.</comment>

<file context>
@@ -338,6 +338,7 @@ def is_port_in_use(
 
         _response_types_map: Dict[str, Optional[str]] = {
             '200': "IsPortInUseResponse",
+            '400': "ErrorResponse",
         }
         response_data = self.api_client.call_api(
</file context>
Fix with cubic

@@ -215,6 +215,16 @@ func (a *LspAPIService) CompletionsExecute(r LspAPICompletionsRequest) (*Complet
body: localVarBody,

@cubic-dev-ai cubic-dev-ai Bot Jul 1, 2026

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P3: Inconsistent indentation in generated code: formatErrorMessage/newErr.model lines use 5 tabs instead of 3. Since this is generated code, the OpenAPI generator template should be fixed upstream.

Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. At toolbox-api-client-go/api_lsp.go, line 225:

<comment>Inconsistent indentation in generated code: `formatErrorMessage`/`newErr.model` lines use 5 tabs instead of 3. Since this is generated code, the OpenAPI generator template should be fixed upstream.</comment>

<file context>
@@ -215,6 +215,16 @@ func (a *LspAPIService) CompletionsExecute(r LspAPICompletionsRequest) (*Complet
+				newErr.error = err.Error()
+				return localVarReturnValue, localVarHTTPResponse, newErr
+			}
+					newErr.error = formatErrorMessage(localVarHTTPResponse.Status, &v)
+					newErr.model = v
+		}
</file context>
Fix with cubic

type DaytonaAuthenticationError struct {
*DaytonaError
// NewDaytonaError builds a DaytonaError with the given message, status code
// and headers. `Source` is left empty — set it explicitly via `SourceSDK`

@cubic-dev-ai cubic-dev-ai Bot Jul 1, 2026

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P3: Doc comment references nonexistent SourceSDK symbol, which is misleading for users.

Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. At sdk-go/pkg/errors/errors.go, line 102:

<comment>Doc comment references nonexistent `SourceSDK` symbol, which is misleading for users.</comment>

<file context>
@@ -26,294 +72,195 @@ func (e *DaytonaError) Error() string {
-type DaytonaAuthenticationError struct {
-	*DaytonaError
+// NewDaytonaError builds a DaytonaError with the given message, status code
+// and headers. `Source` is left empty — set it explicitly via `SourceSDK`
+// for SDK-internal errors, or via the translation layer for server-side
+// errors. Most callers should use this directly; the sentinels below are
</file context>
Fix with cubic

parse_error_body(error.response_body)[:message]
end

def self.string_or_nil(value)

@cubic-dev-ai cubic-dev-ai Bot Jul 1, 2026

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P3: Internal helper methods parse_error_body, parsed_message, error_class_for, and string_or_nil are publicly exposed on the Daytona::Sdk module but are implementation details. Mark them with private_class_method to avoid polluting the public API.

Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. At sdk-ruby/lib/daytona/sdk/errors.rb, line 271:

<comment>Internal helper methods `parse_error_body`, `parsed_message`, `error_class_for`, and `string_or_nil` are publicly exposed on the `Daytona::Sdk` module but are implementation details. Mark them with `private_class_method` to avoid polluting the public API.</comment>

<file context>
@@ -0,0 +1,275 @@
+      parse_error_body(error.response_body)[:message]
+    end
+
+    def self.string_or_nil(value)
+      value.is_a?(String) && !value.empty? ? value : nil
+    end
</file context>
Fix with cubic

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant