Skip to content
Open
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
4 changes: 4 additions & 0 deletions openapi-specs/toolbox.json
Original file line number Diff line number Diff line change
Expand Up @@ -5834,6 +5834,8 @@
"GIT_PUSH_REJECTED",
"GIT_DIRTY_WORKTREE",
"GIT_MERGE_CONFLICT",
"GIT_TRANSPORT_FAILED",
"GIT_REMOTE_REJECTED",
"FILE_NOT_FOUND",
"FILE_ACCESS_DENIED",
"LSP_SERVER_NOT_INITIALIZED",
Expand Down Expand Up @@ -5863,6 +5865,8 @@
"CodeGitPushRejected",
"CodeGitDirtyWorktree",
"CodeGitMergeConflict",
"CodeGitTransportFailed",
"CodeGitRemoteRejected",
"CodeFileNotFound",
"CodeFileAccessDenied",
"CodeLspServerNotInitialized",
Expand Down
6 changes: 6 additions & 0 deletions sdk-go/pkg/errors/errors.go
Original file line number Diff line number Diff line change
Expand Up @@ -155,6 +155,12 @@ var (
ErrGitPushRejected = &DaytonaError{Source: SourceDaemon, Code: "GIT_PUSH_REJECTED"}
ErrGitDirtyWorktree = &DaytonaError{Source: SourceDaemon, Code: "GIT_DIRTY_WORKTREE"}
ErrGitMergeConflict = &DaytonaError{Source: SourceDaemon, Code: "GIT_MERGE_CONFLICT"}
// ErrGitTransportFailed matches network-level git failures: DNS, TLS,
// connection, timeout.
ErrGitTransportFailed = &DaytonaError{Source: SourceDaemon, Code: "GIT_TRANSPORT_FAILED"}
// ErrGitRemoteRejected matches server-side rejections: pre-receive/update
// hooks, branch protection, size/quota limits.
ErrGitRemoteRejected = &DaytonaError{Source: SourceDaemon, Code: "GIT_REMOTE_REJECTED"}

// Daemon: filesystem.
ErrFileNotFound = &DaytonaError{Source: SourceDaemon, Code: "FILE_NOT_FOUND"}
Expand Down
30 changes: 30 additions & 0 deletions sdk-go/pkg/errors/errors_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -107,6 +107,36 @@ func TestErrorsIs_DomainCodeAlsoMatchesParentStatus(t *testing.T) {
}
}

func TestErrorsIs_GitTransportFailedMatchesBadGateway(t *testing.T) {
body := []byte(`{"statusCode":502,"message":"dial tcp: lookup nonexistent.invalid: no such host","code":"GIT_TRANSPORT_FAILED","source":"DAYTONA_DAEMON"}`)
err := sdkerrors.NewDaytonaErrorFromBody(body, http.StatusBadGateway, nil)

if !stderrors.Is(err, sdkerrors.ErrGitTransportFailed) {
t.Fatalf("errors.Is(err, ErrGitTransportFailed) = false")
}
if !stderrors.Is(err, sdkerrors.ErrBadGateway) {
t.Fatalf("errors.Is(err, ErrBadGateway) = false; want true (domain inherits from status)")
}
if stderrors.Is(err, sdkerrors.ErrGitRemoteRejected) {
t.Fatalf("errors.Is(err, ErrGitRemoteRejected) = true; want false (different code)")
}
}

func TestErrorsIs_GitRemoteRejectedMatchesUnprocessableEntity(t *testing.T) {
body := []byte(`{"statusCode":422,"message":"command error on refs/heads/main: pre-receive hook declined","code":"GIT_REMOTE_REJECTED","source":"DAYTONA_DAEMON"}`)
err := sdkerrors.NewDaytonaErrorFromBody(body, http.StatusUnprocessableEntity, nil)

if !stderrors.Is(err, sdkerrors.ErrGitRemoteRejected) {
t.Fatalf("errors.Is(err, ErrGitRemoteRejected) = false")
}
if !stderrors.Is(err, sdkerrors.ErrUnprocessableEntity) {
t.Fatalf("errors.Is(err, ErrUnprocessableEntity) = false; want true (domain inherits from status)")
}
if stderrors.Is(err, sdkerrors.ErrGitTransportFailed) {
t.Fatalf("errors.Is(err, ErrGitTransportFailed) = true; want false (different code)")
}
}

func TestErrorsIs_DomainCodesRequireBothSourceAndCode(t *testing.T) {
body := []byte(`{"statusCode":404,"code":"FILE_NOT_FOUND","source":"DAYTONA_API"}`)
err := sdkerrors.NewDaytonaErrorFromBody(body, http.StatusNotFound, nil)
Expand Down
4 changes: 4 additions & 0 deletions sdk-java/src/main/java/io/daytona/sdk/ExceptionMapper.java
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,9 @@
import io.daytona.sdk.exception.DaytonaGitDirtyWorktreeException;
import io.daytona.sdk.exception.DaytonaGitMergeConflictException;
import io.daytona.sdk.exception.DaytonaGitPushRejectedException;
import io.daytona.sdk.exception.DaytonaGitRemoteRejectedException;
import io.daytona.sdk.exception.DaytonaGitRepoNotFoundException;
import io.daytona.sdk.exception.DaytonaGitTransportFailedException;
import io.daytona.sdk.exception.DaytonaGoneException;
import io.daytona.sdk.exception.DaytonaInternalServerException;
import io.daytona.sdk.exception.DaytonaLspServerNotInitializedException;
Expand Down Expand Up @@ -204,6 +206,8 @@ private static Map<String, ErrorFactory> buildCodeMap() {
map.put(SRC_DAEMON + "|GIT_PUSH_REJECTED", DaytonaGitPushRejectedException::new);
map.put(SRC_DAEMON + "|GIT_DIRTY_WORKTREE", DaytonaGitDirtyWorktreeException::new);
map.put(SRC_DAEMON + "|GIT_MERGE_CONFLICT", DaytonaGitMergeConflictException::new);
map.put(SRC_DAEMON + "|GIT_TRANSPORT_FAILED", DaytonaGitTransportFailedException::new);
map.put(SRC_DAEMON + "|GIT_REMOTE_REJECTED", DaytonaGitRemoteRejectedException::new);

// Daemon: filesystem
map.put(SRC_DAEMON + "|FILE_NOT_FOUND", DaytonaFileNotFoundException::new);
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
// Copyright Daytona Platforms Inc.
// SPDX-License-Identifier: Apache-2.0

package io.daytona.sdk.exception;
/**
* The git remote rejected the operation (hooks, branch protection or quota).
*
* <p>Subclass of {@link DaytonaUnprocessableEntityException}.
*/
public class DaytonaGitRemoteRejectedException extends DaytonaUnprocessableEntityException {
public DaytonaGitRemoteRejectedException(String message) {
super(message);
}

public DaytonaGitRemoteRejectedException(String message, Throwable cause) {
super(message, cause);
}

public DaytonaGitRemoteRejectedException(String message, String code, String source) {
super(message, code, source);
}

public DaytonaGitRemoteRejectedException(String message, Throwable cause, String code, String source) {
super(message, cause, code, source);
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
// Copyright Daytona Platforms Inc.
// SPDX-License-Identifier: Apache-2.0

package io.daytona.sdk.exception;
/**
* The git remote was unreachable (DNS, TLS, connection or timeout failure).
*
* <p>Subclass of {@link DaytonaBadGatewayException}.
*/
public class DaytonaGitTransportFailedException extends DaytonaBadGatewayException {
public DaytonaGitTransportFailedException(String message) {
super(message);
}

public DaytonaGitTransportFailedException(String message, Throwable cause) {
super(message, cause);
}

public DaytonaGitTransportFailedException(String message, String code, String source) {
super(message, code, source);
}

public DaytonaGitTransportFailedException(String message, Throwable cause, String code, String source) {
super(message, cause, code, source);
}
}
32 changes: 32 additions & 0 deletions sdk-java/src/test/java/io/daytona/sdk/ExceptionMapperTest.java
Original file line number Diff line number Diff line change
Expand Up @@ -270,6 +270,38 @@ void domainCodeOverridesStatusCode() {
});
}

@Test
void gitTransportFailedMapsToDomainClass() {
assertThatThrownBy(() -> ExceptionMapper.callMain(() -> {
throw new io.daytona.api.client.ApiException(
502, "bad gateway", null,
"{\"message\":\"dial tcp: lookup nonexistent.invalid: no such host\",\"code\":\"GIT_TRANSPORT_FAILED\",\"source\":\"DAYTONA_DAEMON\"}");
}))
.isInstanceOf(io.daytona.sdk.exception.DaytonaGitTransportFailedException.class)
.isInstanceOf(io.daytona.sdk.exception.DaytonaBadGatewayException.class)
.satisfies(error -> {
DaytonaException exception = (DaytonaException) error;
assertThat(exception.getCode()).isEqualTo("GIT_TRANSPORT_FAILED");
assertThat(exception.getSource()).isEqualTo("DAYTONA_DAEMON");
});
}

@Test
void gitRemoteRejectedMapsToDomainClass() {
assertThatThrownBy(() -> ExceptionMapper.callMain(() -> {
throw new io.daytona.api.client.ApiException(
422, "unprocessable", null,
"{\"message\":\"command error on refs/heads/main: pre-receive hook declined\",\"code\":\"GIT_REMOTE_REJECTED\",\"source\":\"DAYTONA_DAEMON\"}");
}))
.isInstanceOf(io.daytona.sdk.exception.DaytonaGitRemoteRejectedException.class)
.isInstanceOf(io.daytona.sdk.exception.DaytonaUnprocessableEntityException.class)
.satisfies(error -> {
DaytonaException exception = (DaytonaException) error;
assertThat(exception.getCode()).isEqualTo("GIT_REMOTE_REJECTED");
assertThat(exception.getSource()).isEqualTo("DAYTONA_DAEMON");
});
}

@Test
void unknownCodeFallsBackToStatusClass() {
assertThatThrownBy(() -> ExceptionMapper.callMain(() -> {
Expand Down
6 changes: 6 additions & 0 deletions sdk-python/src/daytona/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -66,7 +66,9 @@
DaytonaGitDirtyWorktreeError,
DaytonaGitMergeConflictError,
DaytonaGitPushRejectedError,
DaytonaGitRemoteRejectedError,
DaytonaGitRepoNotFoundError,
DaytonaGitTransportFailedError,
DaytonaGoneError,
DaytonaInternalServerError,
DaytonaLspServerNotInitializedError,
Expand Down Expand Up @@ -183,6 +185,8 @@
"DaytonaGitPushRejectedError",
"DaytonaGitDirtyWorktreeError",
"DaytonaGitMergeConflictError",
"DaytonaGitTransportFailedError",
"DaytonaGitRemoteRejectedError",
"DaytonaFileNotFoundError",
"DaytonaFileAccessDeniedError",
"DaytonaLspServerNotInitializedError",
Expand Down Expand Up @@ -268,6 +272,8 @@
"DaytonaGitPushRejectedError": "common.errors",
"DaytonaGitDirtyWorktreeError": "common.errors",
"DaytonaGitMergeConflictError": "common.errors",
"DaytonaGitTransportFailedError": "common.errors",
"DaytonaGitRemoteRejectedError": "common.errors",
"DaytonaFileNotFoundError": "common.errors",
"DaytonaFileAccessDeniedError": "common.errors",
"DaytonaLspServerNotInitializedError": "common.errors",
Expand Down
10 changes: 10 additions & 0 deletions sdk-python/src/daytona/common/errors.py
Original file line number Diff line number Diff line change
Expand Up @@ -259,6 +259,14 @@ class DaytonaGitMergeConflictError(DaytonaConflictError):
"""Git merge has conflicts that need manual resolution."""


class DaytonaGitTransportFailedError(DaytonaBadGatewayError):
"""The git remote was unreachable (DNS, TLS, connection or timeout failure)."""


class DaytonaGitRemoteRejectedError(DaytonaUnprocessableEntityError):
"""The git remote rejected the operation (hooks, branch protection or quota)."""


# Filesystem.
class DaytonaFileNotFoundError(DaytonaNotFoundError):
"""Filesystem entry was not found."""
Expand Down Expand Up @@ -335,6 +343,8 @@ class DaytonaRecordingFfmpegNotFoundError(DaytonaServiceUnavailableError):
(SOURCE_DAEMON, "GIT_PUSH_REJECTED"): DaytonaGitPushRejectedError,
(SOURCE_DAEMON, "GIT_DIRTY_WORKTREE"): DaytonaGitDirtyWorktreeError,
(SOURCE_DAEMON, "GIT_MERGE_CONFLICT"): DaytonaGitMergeConflictError,
(SOURCE_DAEMON, "GIT_TRANSPORT_FAILED"): DaytonaGitTransportFailedError,
(SOURCE_DAEMON, "GIT_REMOTE_REJECTED"): DaytonaGitRemoteRejectedError,
# Daemon: filesystem
(SOURCE_DAEMON, "FILE_NOT_FOUND"): DaytonaFileNotFoundError,
(SOURCE_DAEMON, "FILE_ACCESS_DENIED"): DaytonaFileAccessDeniedError,
Expand Down
26 changes: 26 additions & 0 deletions sdk-python/tests/test_errors.py
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,8 @@
DaytonaConnectionError,
DaytonaConnectionTimeoutError,
DaytonaError,
DaytonaGitRemoteRejectedError,
DaytonaGitTransportFailedError,
DaytonaGoneError,
DaytonaInternalServerError,
DaytonaNotFoundError,
Expand Down Expand Up @@ -129,6 +131,30 @@ def test_create_daytona_error_uses_specific_subclass(self):
assert isinstance(error, DaytonaNotFoundError)
assert error.code == "NOT_FOUND"

def test_git_transport_failed_routes_to_bad_gateway_subclass(self):
error = create_daytona_error(
"dns lookup failed",
status_code=502,
code="GIT_TRANSPORT_FAILED",
source="DAYTONA_DAEMON",
)

assert isinstance(error, DaytonaGitTransportFailedError)
assert isinstance(error, DaytonaBadGatewayError)
assert error.code == "GIT_TRANSPORT_FAILED"

def test_git_remote_rejected_routes_to_unprocessable_subclass(self):
error = create_daytona_error(
"pre-receive hook declined",
status_code=422,
code="GIT_REMOTE_REJECTED",
source="DAYTONA_DAEMON",
)

assert isinstance(error, DaytonaGitRemoteRejectedError)
assert isinstance(error, DaytonaUnprocessableEntityError)
assert error.code == "GIT_REMOTE_REJECTED"


class TestStatusCodeClassification:
"""Every HTTP status code that Daytona services actually emit has a typed
Expand Down
4 changes: 4 additions & 0 deletions sdk-ruby/lib/daytona/sdk/errors.rb
Original file line number Diff line number Diff line change
Expand Up @@ -120,6 +120,8 @@ class GitBranchExistsError < ConflictError; end
class GitPushRejectedError < ConflictError; end
class GitDirtyWorktreeError < ConflictError; end
class GitMergeConflictError < ConflictError; end
class GitTransportFailedError < BadGatewayError; end
class GitRemoteRejectedError < UnprocessableEntityError; end

# Daemon: filesystem
class FileNotFoundError < NotFoundError; end
Expand Down Expand Up @@ -171,6 +173,8 @@ class RecordingFfmpegNotFoundError < ServiceUnavailableError; end
[SOURCE_DAEMON, 'GIT_PUSH_REJECTED'] => GitPushRejectedError,
[SOURCE_DAEMON, 'GIT_DIRTY_WORKTREE'] => GitDirtyWorktreeError,
[SOURCE_DAEMON, 'GIT_MERGE_CONFLICT'] => GitMergeConflictError,
[SOURCE_DAEMON, 'GIT_TRANSPORT_FAILED'] => GitTransportFailedError,
[SOURCE_DAEMON, 'GIT_REMOTE_REJECTED'] => GitRemoteRejectedError,

# Daemon: filesystem
[SOURCE_DAEMON, 'FILE_NOT_FOUND'] => FileNotFoundError,
Expand Down
20 changes: 20 additions & 0 deletions sdk-ruby/spec/daytona/sdk/errors_spec.rb
Original file line number Diff line number Diff line change
Expand Up @@ -66,6 +66,8 @@
described_class::GitPushRejectedError => described_class::ConflictError,
described_class::GitDirtyWorktreeError => described_class::ConflictError,
described_class::GitMergeConflictError => described_class::ConflictError,
described_class::GitTransportFailedError => described_class::BadGatewayError,
described_class::GitRemoteRejectedError => described_class::UnprocessableEntityError,
described_class::FileNotFoundError => described_class::NotFoundError,
described_class::FileAccessDeniedError => described_class::ForbiddenError,
described_class::LspServerNotInitializedError => described_class::ValidationError,
Expand Down Expand Up @@ -151,6 +153,24 @@ def api_error(status, body)
expect(err.source).to eq('DAYTONA_DAEMON')
end

it 'routes GIT_TRANSPORT_FAILED to GitTransportFailedError' do
body = '{"message":"dial tcp: lookup nonexistent.invalid: no such host","code":"GIT_TRANSPORT_FAILED","source":"DAYTONA_DAEMON"}'
err = described_class.wrap_error(api_error(502, body))

expect(err).to be_a(described_class::GitTransportFailedError)
expect(err).to be_a(described_class::BadGatewayError) # inheritance
expect(err.code).to eq('GIT_TRANSPORT_FAILED')
end

it 'routes GIT_REMOTE_REJECTED to GitRemoteRejectedError' do
body = '{"message":"command error on refs/heads/main: pre-receive hook declined","code":"GIT_REMOTE_REJECTED","source":"DAYTONA_DAEMON"}'
err = described_class.wrap_error(api_error(422, body))

expect(err).to be_a(described_class::GitRemoteRejectedError)
expect(err).to be_a(described_class::UnprocessableEntityError) # inheritance
expect(err.code).to eq('GIT_REMOTE_REJECTED')
end

it 'prepends the prefix to the message' do
err = described_class.wrap_error(api_error(409, '{"message":"exists"}'), 'Failed to add branch')

Expand Down
16 changes: 16 additions & 0 deletions sdk-typescript/src/__tests__/DaytonaError.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,8 @@ import {
DaytonaError,
DaytonaFileNotFoundError,
DaytonaGitAuthFailedError,
DaytonaGitRemoteRejectedError,
DaytonaGitTransportFailedError,
DaytonaGoneError,
DaytonaInternalServerError,
DaytonaNotFoundError,
Expand Down Expand Up @@ -113,6 +115,20 @@ describe('Domain code classification with status-class inheritance', () => {
expect(err).toBeInstanceOf(DaytonaServiceUnavailableError)
})

it('daemon GIT_TRANSPORT_FAILED inherits from DaytonaBadGatewayError', () => {
const err = createDaytonaError('dns lookup failed', 502, undefined, 'GIT_TRANSPORT_FAILED', 'DAYTONA_DAEMON')
expect(err).toBeInstanceOf(DaytonaGitTransportFailedError)
expect(err).toBeInstanceOf(DaytonaBadGatewayError)
expect(err.code).toBe('GIT_TRANSPORT_FAILED')
})

it('daemon GIT_REMOTE_REJECTED inherits from DaytonaUnprocessableEntityError', () => {
const err = createDaytonaError('pre-receive hook declined', 422, undefined, 'GIT_REMOTE_REJECTED', 'DAYTONA_DAEMON')
expect(err).toBeInstanceOf(DaytonaGitRemoteRejectedError)
expect(err).toBeInstanceOf(DaytonaUnprocessableEntityError)
expect(err.code).toBe('GIT_REMOTE_REJECTED')
})

it('falls back to status class when (source, code) is unknown', () => {
const err = createDaytonaError('mystery 404', 404, undefined, 'UNKNOWN_CODE', 'DAYTONA_DAEMON')
expect(err).toBeInstanceOf(DaytonaNotFoundError)
Expand Down
6 changes: 6 additions & 0 deletions sdk-typescript/src/errors/DaytonaError.ts
Original file line number Diff line number Diff line change
Expand Up @@ -125,6 +125,10 @@ export class DaytonaGitPushRejectedError extends DaytonaConflictError {}
export class DaytonaGitDirtyWorktreeError extends DaytonaConflictError {}
/** A git merge produced conflicts (code `GIT_MERGE_CONFLICT`). */
export class DaytonaGitMergeConflictError extends DaytonaConflictError {}
/** The git remote was unreachable — DNS, TLS, connection or timeout failure (code `GIT_TRANSPORT_FAILED`). */
export class DaytonaGitTransportFailedError extends DaytonaBadGatewayError {}
/** The git remote rejected the operation — hooks, branch protection or quota (code `GIT_REMOTE_REJECTED`). */
export class DaytonaGitRemoteRejectedError extends DaytonaUnprocessableEntityError {}

// --- Filesystem (daemon) ---
/** The file does not exist in the sandbox (code `FILE_NOT_FOUND`). */
Expand Down Expand Up @@ -171,6 +175,8 @@ const CODE_TO_ERROR_CLASS: Record<string, typeof DaytonaError> = {
'DAYTONA_DAEMON|GIT_PUSH_REJECTED': DaytonaGitPushRejectedError,
'DAYTONA_DAEMON|GIT_DIRTY_WORKTREE': DaytonaGitDirtyWorktreeError,
'DAYTONA_DAEMON|GIT_MERGE_CONFLICT': DaytonaGitMergeConflictError,
'DAYTONA_DAEMON|GIT_TRANSPORT_FAILED': DaytonaGitTransportFailedError,
'DAYTONA_DAEMON|GIT_REMOTE_REJECTED': DaytonaGitRemoteRejectedError,
'DAYTONA_DAEMON|FILE_NOT_FOUND': DaytonaFileNotFoundError,
'DAYTONA_DAEMON|FILE_ACCESS_DENIED': DaytonaFileAccessDeniedError,
'DAYTONA_DAEMON|LSP_SERVER_NOT_INITIALIZED': DaytonaLspServerNotInitializedError,
Expand Down
2 changes: 2 additions & 0 deletions sdk-typescript/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -64,6 +64,8 @@ export {
DaytonaGitPushRejectedError,
DaytonaGitDirtyWorktreeError,
DaytonaGitMergeConflictError,
DaytonaGitTransportFailedError,
DaytonaGitRemoteRejectedError,
DaytonaFileNotFoundError,
DaytonaFileAccessDeniedError,
DaytonaLspServerNotInitializedError,
Expand Down
Loading
Loading