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
39 changes: 39 additions & 0 deletions apierror/apierror.go
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,8 @@ package apierror
import (
"errors"
"fmt"
"log/slog"
"net/http"

apiClient "github.com/smartcontractkit/crec-api-go/client"
)
Expand All @@ -14,6 +16,11 @@ import (
// ApplicationError of type ORGANIZATION_NOT_FOUND).
var ErrOrganizationNotFound = errors.New("organization not found")

// ErrPermissionDenied is returned when the CREC API reports that the
// authenticated principal lacks the required permission for the operation
// (HTTP 403 with an ApplicationError of type PERMISSION_DENIED).
var ErrPermissionDenied = errors.New("permission denied")

// Canonical not-found sentinels for HTTP 404 responses. The API disambiguates
// which resource was missing via ApplicationError.code. Packages that assign
// these variables (rather than defining their own) share the same sentinel
Expand Down Expand Up @@ -51,6 +58,8 @@ func FromApplicationError(appErr *apiClient.ApplicationError) error {
switch appErr.Type {
case apiClient.ORGANIZATIONNOTFOUND:
return ErrOrganizationNotFound
case apiClient.PERMISSIONDENIED:
return ErrPermissionDenied
default:
return nil
}
Expand All @@ -67,6 +76,36 @@ func Wrap(appErr *apiClient.ApplicationError, opErr error, statusCode int) error
return fmt.Errorf("%w: %w (status code %d)", opErr, ErrUnexpectedStatusCode, statusCode)
}

// HandleErrorStatus handles the common HTTP error cases shared across all SDK endpoints.
// Endpoints handle their success and specific cases (404, 409, 429, etc.) in
// their own switch and delegate the remaining cases to this helper via default.
func HandleErrorStatus(
statusCode int,
json401, json403 *apiClient.ApplicationError,
opErr error,
opDesc string,
body []byte,
logger *slog.Logger,
) error {
switch statusCode {
case http.StatusForbidden:
logger.Error("Permission denied when "+opDesc,
"status_code", statusCode,
"body", string(body))
return Wrap(json403, opErr, statusCode)
case http.StatusUnauthorized:
logger.Error("Unauthorized when "+opDesc,
"status_code", statusCode,
"body", string(body))
return Wrap(json401, opErr, statusCode)
default:
logger.Error("Unexpected status code when "+opDesc,
"status_code", statusCode,
"body", string(body))
return fmt.Errorf("%w: %w (status code %d)", opErr, ErrUnexpectedStatusCode, statusCode)
}
}

// NotFound maps a 404 ApplicationError to its canonical not-found sentinel based
// on ApplicationError.code, or returns nil when the code is missing or
// unrecognized (forward-compatible for codes added after this SDK release).
Expand Down
59 changes: 59 additions & 0 deletions apierror/apierror_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ package apierror_test

import (
"errors"
"log/slog"
"net/http"
"testing"

Expand All @@ -27,6 +28,11 @@ func TestApierror_FromApplicationError(t *testing.T) {
appErr: &apiClient.ApplicationError{Type: apiClient.ORGANIZATIONNOTFOUND, Message: "organization not found"},
wantErr: apierror.ErrOrganizationNotFound,
},
{
name: "permission denied maps to sentinel",
appErr: &apiClient.ApplicationError{Type: apiClient.PERMISSIONDENIED, Message: "principal lacks permission crec:wallet:create"},
wantErr: apierror.ErrPermissionDenied,
},
{
name: "unknown future type degrades to nil",
appErr: &apiClient.ApplicationError{Type: "SOME_FUTURE_TYPE", Message: "new"},
Expand Down Expand Up @@ -208,6 +214,15 @@ func TestApierror_Wrap(t *testing.T) {
assert.NotErrorIs(t, err, apierror.ErrUnexpectedStatusCode)
})

t.Run("permission denied wraps opErr", func(t *testing.T) {
appErr := &apiClient.ApplicationError{Type: apiClient.PERMISSIONDENIED, Message: "principal lacks permission crec:wallet:create"}
err := apierror.Wrap(appErr, opErr, http.StatusForbidden)

assert.ErrorIs(t, err, opErr)
assert.ErrorIs(t, err, apierror.ErrPermissionDenied)
assert.NotErrorIs(t, err, apierror.ErrUnexpectedStatusCode)
})

t.Run("unmapped type falls back to unexpected-status error", func(t *testing.T) {
appErr := &apiClient.ApplicationError{Type: "SOME_FUTURE_TYPE", Message: "new"}
err := apierror.Wrap(appErr, opErr, http.StatusUnauthorized)
Expand All @@ -224,3 +239,47 @@ func TestApierror_Wrap(t *testing.T) {
assert.ErrorIs(t, err, apierror.ErrUnexpectedStatusCode)
})
}

func TestApierror_HandleErrorStatus(t *testing.T) {
opErr := errors.New("failed to create wallet")
logger := slog.New(slog.DiscardHandler)

t.Run("403 with PERMISSION_DENIED wraps with ErrPermissionDenied", func(t *testing.T) {
json403 := &apiClient.ApplicationError{Type: apiClient.PERMISSIONDENIED, Message: "principal lacks permission crec:wallet:create"}
err := apierror.HandleErrorStatus(http.StatusForbidden, nil, json403, opErr, "creating wallet", []byte("body"), logger)

assert.ErrorIs(t, err, opErr)
assert.ErrorIs(t, err, apierror.ErrPermissionDenied)
assert.NotErrorIs(t, err, apierror.ErrUnexpectedStatusCode)
})

t.Run("401 with ORGANIZATION_NOT_FOUND wraps with ErrOrganizationNotFound", func(t *testing.T) {
json401 := &apiClient.ApplicationError{Type: apiClient.ORGANIZATIONNOTFOUND, Message: "organization not found"}
err := apierror.HandleErrorStatus(http.StatusUnauthorized, json401, nil, opErr, "creating wallet", []byte("body"), logger)

assert.ErrorIs(t, err, opErr)
assert.ErrorIs(t, err, apierror.ErrOrganizationNotFound)
assert.NotErrorIs(t, err, apierror.ErrUnexpectedStatusCode)
})

t.Run("401 with unmapped type falls back to ErrUnexpectedStatusCode", func(t *testing.T) {
json401 := &apiClient.ApplicationError{Type: "SOME_FUTURE_TYPE", Message: "new"}
err := apierror.HandleErrorStatus(http.StatusUnauthorized, json401, nil, opErr, "creating wallet", []byte("body"), logger)

assert.ErrorIs(t, err, opErr)
assert.ErrorIs(t, err, apierror.ErrUnexpectedStatusCode)
})

t.Run("500 falls back to ErrUnexpectedStatusCode", func(t *testing.T) {
err := apierror.HandleErrorStatus(http.StatusInternalServerError, nil, nil, opErr, "creating wallet", []byte("body"), logger)

assert.ErrorIs(t, err, opErr)
assert.ErrorIs(t, err, apierror.ErrUnexpectedStatusCode)
})

t.Run("nil json401 and json403 do not panic", func(t *testing.T) {
err := apierror.HandleErrorStatus(http.StatusUnauthorized, nil, nil, opErr, "creating wallet", nil, logger)
assert.ErrorIs(t, err, opErr)
assert.ErrorIs(t, err, apierror.ErrUnexpectedStatusCode)
})
}
50 changes: 5 additions & 45 deletions channels/channels.go
Original file line number Diff line number Diff line change
Expand Up @@ -137,16 +137,8 @@ func (c *Client) Create(ctx context.Context, input CreateInput) (*apiClient.Chan
"channel_id", resp.JSON201.ChannelId.String(),
"name", resp.JSON201.Name)
return resp.JSON201, nil
case http.StatusUnauthorized:
c.logger.Error("Unauthorized when creating channel",
"status_code", resp.StatusCode(),
"body", string(resp.Body))
return nil, apierror.Wrap(resp.JSON401, ErrCreateChannel, resp.StatusCode())
default:
c.logger.Error("Unexpected status code when creating channel",
"status_code", resp.StatusCode(),
"body", string(resp.Body))
return nil, fmt.Errorf("%w: %w (status code %d)", ErrCreateChannel, apierror.ErrUnexpectedStatusCode, resp.StatusCode())
return nil, apierror.HandleErrorStatus(resp.StatusCode(), resp.JSON401, resp.JSON403, ErrCreateChannel, "creating channel", resp.Body, c.logger)
}
}

Expand Down Expand Up @@ -186,16 +178,8 @@ func (c *Client) Get(ctx context.Context, channelID uuid.UUID) (*apiClient.Chann
"code", apierror.NotFoundCode(resp.JSON404),
)
return nil, fmt.Errorf("%w: channel ID %s", ErrChannelNotFound, channelID.String())
case http.StatusUnauthorized:
c.logger.Error("Unauthorized when getting channel",
"status_code", resp.StatusCode(),
"body", string(resp.Body))
return nil, apierror.Wrap(resp.JSON401, ErrGetChannel, resp.StatusCode())
default:
c.logger.Error("Unexpected status code when getting channel",
"status_code", resp.StatusCode(),
"body", string(resp.Body))
return nil, fmt.Errorf("%w: %w (status code %d)", ErrGetChannel, apierror.ErrUnexpectedStatusCode, resp.StatusCode())
return nil, apierror.HandleErrorStatus(resp.StatusCode(), resp.JSON401, resp.JSON403, ErrGetChannel, "getting channel", resp.Body, c.logger)
}
}

Expand Down Expand Up @@ -247,16 +231,8 @@ func (c *Client) List(ctx context.Context, input ListInput) ([]apiClient.Channel
"count", len(resp.JSON200.Data),
"has_more", resp.JSON200.HasMore)
return resp.JSON200.Data, resp.JSON200.HasMore, nil
case http.StatusUnauthorized:
c.logger.Error("Unauthorized when listing channels",
"status_code", resp.StatusCode(),
"body", string(resp.Body))
return nil, false, apierror.Wrap(resp.JSON401, ErrListChannels, resp.StatusCode())
default:
c.logger.Error("Unexpected status code when listing channels",
"status_code", resp.StatusCode(),
"body", string(resp.Body))
return nil, false, fmt.Errorf("%w: %w (status code %d)", ErrListChannels, apierror.ErrUnexpectedStatusCode, resp.StatusCode())
return nil, false, apierror.HandleErrorStatus(resp.StatusCode(), resp.JSON401, resp.JSON403, ErrListChannels, "listing channels", resp.Body, c.logger)
}
}

Expand Down Expand Up @@ -314,16 +290,8 @@ func (c *Client) Update(ctx context.Context, channelID uuid.UUID, input UpdateIn
"code", apierror.NotFoundCode(resp.JSON404),
)
return nil, fmt.Errorf("%w: channel ID %s", ErrChannelNotFound, channelID.String())
case http.StatusUnauthorized:
c.logger.Error("Unauthorized when updating channel",
"status_code", resp.StatusCode(),
"body", string(resp.Body))
return nil, apierror.Wrap(resp.JSON401, ErrUpdateChannel, resp.StatusCode())
default:
c.logger.Error("Unexpected status code when updating channel",
"status_code", resp.StatusCode(),
"body", string(resp.Body))
return nil, fmt.Errorf("%w: %w (status code %d)", ErrUpdateChannel, apierror.ErrUnexpectedStatusCode, resp.StatusCode())
return nil, apierror.HandleErrorStatus(resp.StatusCode(), resp.JSON401, resp.JSON403, ErrUpdateChannel, "updating channel", resp.Body, c.logger)
}
}

Expand Down Expand Up @@ -368,15 +336,7 @@ func (c *Client) Archive(ctx context.Context, channelID uuid.UUID) (*apiClient.C
"code", apierror.NotFoundCode(resp.JSON404),
)
return nil, fmt.Errorf("%w: channel ID %s", ErrChannelNotFound, channelID.String())
case http.StatusUnauthorized:
c.logger.Error("Unauthorized when archiving channel",
"status_code", resp.StatusCode(),
"body", string(resp.Body))
return nil, apierror.Wrap(resp.JSON401, ErrArchiveChannel, resp.StatusCode())
default:
c.logger.Error("Unexpected status code when archiving channel",
"status_code", resp.StatusCode(),
"body", string(resp.Body))
return nil, fmt.Errorf("%w: %w (status code %d)", ErrArchiveChannel, apierror.ErrUnexpectedStatusCode, resp.StatusCode())
return nil, apierror.HandleErrorStatus(resp.StatusCode(), resp.JSON401, resp.JSON403, ErrArchiveChannel, "archiving channel", resp.Body, c.logger)
}
}
13 changes: 1 addition & 12 deletions crec.go
Original file line number Diff line number Diff line change
Expand Up @@ -270,18 +270,7 @@ func (c *Client) ListNetworks(ctx context.Context) ([]apiClient.Network, bool, e
return nil, false, ErrListNetworks
}
return resp.JSON200.Data, resp.JSON200.HasMore, nil
case http.StatusUnauthorized:
c.logger.Error("Unauthorized when listing networks",
"status_code", resp.StatusCode(),
"body", string(resp.Body))
if mapped := apierror.FromApplicationError(resp.JSON401); mapped != nil {
return nil, false, fmt.Errorf("%w: %w", ErrListNetworks, mapped)
}
return nil, false, fmt.Errorf("%w (status code %d)", ErrListNetworks, resp.StatusCode())
default:
c.logger.Error("Unexpected status code when listing networks",
"status_code", resp.StatusCode(),
"body", string(resp.Body))
return nil, false, fmt.Errorf("%w (status code %d)", ErrListNetworks, resp.StatusCode())
return nil, false, apierror.HandleErrorStatus(resp.StatusCode(), resp.JSON401, resp.JSON403, ErrListNetworks, "listing networks", resp.Body, c.logger)
}
}
32 changes: 2 additions & 30 deletions events/events.go
Original file line number Diff line number Diff line change
Expand Up @@ -288,22 +288,8 @@ func (c *Client) Poll(
return nil, false, apierror.WrapChannelNotFound(
resp.JSON404, ErrPollEvents, "channel ID "+channelID.String(),
)
case http.StatusUnauthorized:
c.logger.Error(
"Failed to get events - unauthorized",
"status_code", resp.StatusCode(),
"body", string(resp.Body),
)
return nil, false, apierror.Wrap(resp.JSON401, ErrPollEvents, resp.StatusCode())
default:
c.logger.Error(
"Failed to get events - unexpected status code",
"status_code", resp.StatusCode(),
"body", string(resp.Body),
)
return nil, false, fmt.Errorf(
"%w: %w (status code %d)", ErrPollEvents, apierror.ErrUnexpectedStatusCode, resp.StatusCode(),
)
return nil, false, apierror.HandleErrorStatus(resp.StatusCode(), resp.JSON401, resp.JSON403, ErrPollEvents, "polling events", resp.Body, c.logger)
}
}

Expand Down Expand Up @@ -370,22 +356,8 @@ func (c *Client) SearchEvents(
return nil, false, fmt.Errorf(
"%w: %w: %s (status code %d)", ErrSearchEvents, ErrBadRequest, errorMsg, resp.StatusCode(),
)
case http.StatusUnauthorized:
c.logger.Error(
"Failed to search events - unauthorized",
"status_code", resp.StatusCode(),
"body", string(resp.Body),
)
return nil, false, apierror.Wrap(resp.JSON401, ErrSearchEvents, resp.StatusCode())
default:
c.logger.Error(
"Failed to search events - unexpected status code",
"status_code", resp.StatusCode(),
"body", string(resp.Body),
)
return nil, false, fmt.Errorf(
"%w: %w (status code %d)", ErrSearchEvents, apierror.ErrUnexpectedStatusCode, resp.StatusCode(),
)
return nil, false, apierror.HandleErrorStatus(resp.StatusCode(), resp.JSON401, resp.JSON403, ErrSearchEvents, "searching events", resp.Body, c.logger)
}
}

Expand Down
2 changes: 1 addition & 1 deletion go.mod
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,7 @@ require (
github.com/oapi-codegen/runtime v1.1.2
github.com/smartcontractkit/chain-selectors v1.0.89
github.com/smartcontractkit/chainlink-common v0.10.0
github.com/smartcontractkit/crec-api-go v0.8.0-rc1
github.com/smartcontractkit/crec-api-go v0.8.0-rc2
github.com/stretchr/testify v1.11.1
github.com/testcontainers/testcontainers-go v0.38.0
github.com/testcontainers/testcontainers-go/modules/vault v0.38.0
Expand Down
4 changes: 2 additions & 2 deletions go.sum
Original file line number Diff line number Diff line change
Expand Up @@ -435,8 +435,8 @@ github.com/smartcontractkit/chainlink-common v0.10.0 h1:d90b9UPJecrIryzhl43F1oQw
github.com/smartcontractkit/chainlink-common v0.10.0/go.mod h1:13YN2kb3Vqpw2S7d4IwhX/578WPGC0JHN5JrOnAEsOc=
github.com/smartcontractkit/chainlink-protos/cre/go v0.0.0-20260210221717-2546aed27ebe h1:Vc4zoSc/j6/FdCQ7vcyHTTB7kzHI2f+lHCHqFuiCcJQ=
github.com/smartcontractkit/chainlink-protos/cre/go v0.0.0-20260210221717-2546aed27ebe/go.mod h1:Jqt53s27Tr0jDl8mdBXg1xhu6F8Fci8JOuq43tgHOM8=
github.com/smartcontractkit/crec-api-go v0.8.0-rc1 h1:qEhP1q+fBdoIOrZdRuqw3aUaS7np7sSO2gVYdMA4s/M=
github.com/smartcontractkit/crec-api-go v0.8.0-rc1/go.mod h1:y91qqcZFtWiKLFu66c/dmBp10bTzcpGcb4XFR7eLknk=
github.com/smartcontractkit/crec-api-go v0.8.0-rc2 h1:YuhcGkl8/1umaFiw718ahIb1+eJds4QFdWbrUd7472o=
github.com/smartcontractkit/crec-api-go v0.8.0-rc2/go.mod h1:y91qqcZFtWiKLFu66c/dmBp10bTzcpGcb4XFR7eLknk=
github.com/smartcontractkit/libocr v0.0.0-20250912173940-f3ab0246e23d h1:LokA9PoCNb8mm8mDT52c3RECPMRsGz1eCQORq+J3n74=
github.com/smartcontractkit/libocr v0.0.0-20250912173940-f3ab0246e23d/go.mod h1:Acy3BTBxou83ooMESLO90s8PKSu7RvLCzwSTbxxfOK0=
github.com/sourcegraph/conc v0.3.0 h1:OQTbbt6P72L20UqAkXXuLOj79LfEanQ+YQFNpLA9ySo=
Expand Down
30 changes: 3 additions & 27 deletions queries/queries.go
Original file line number Diff line number Diff line change
Expand Up @@ -303,16 +303,8 @@ func (c *Client) Create(ctx context.Context, input CreateInput) (*apiClient.Quer
return nil, fmt.Errorf("%w: %w", ErrCreateQuery, ErrIdempotencyConflict)
case http.StatusTooManyRequests:
return nil, fmt.Errorf("%w: %w", ErrCreateQuery, ErrRateLimitExceeded)
case http.StatusUnauthorized:
c.logger.Error("Unauthorized when creating query",
"status_code", resp.StatusCode(),
"body", string(resp.Body))
return nil, apierror.Wrap(resp.JSON401, ErrCreateQuery, resp.StatusCode())
default:
c.logger.Error("Unexpected status code when creating query",
"status_code", resp.StatusCode(),
"body", string(resp.Body))
return nil, fmt.Errorf("%w: %w (status code %d)", ErrCreateQuery, apierror.ErrUnexpectedStatusCode, resp.StatusCode())
return nil, apierror.HandleErrorStatus(resp.StatusCode(), resp.JSON401, resp.JSON403, ErrCreateQuery, "creating query", resp.Body, c.logger)
}
}

Expand Down Expand Up @@ -369,16 +361,8 @@ func (c *Client) Get(ctx context.Context, channelID uuid.UUID, queryID uuid.UUID
ErrGetQuery,
fmt.Sprintf("channel ID %s, query ID %s", channelID.String(), queryID.String()),
)
case http.StatusUnauthorized:
c.logger.Error("Unauthorized when getting query",
"status_code", resp.StatusCode(),
"body", string(resp.Body))
return nil, apierror.Wrap(resp.JSON401, ErrGetQuery, resp.StatusCode())
default:
c.logger.Error("Unexpected status code when getting query",
"status_code", resp.StatusCode(),
"body", string(resp.Body))
return nil, fmt.Errorf("%w: %w (status code %d)", ErrGetQuery, apierror.ErrUnexpectedStatusCode, resp.StatusCode())
return nil, apierror.HandleErrorStatus(resp.StatusCode(), resp.JSON401, resp.JSON403, ErrGetQuery, "getting query", resp.Body, c.logger)
}
}

Expand Down Expand Up @@ -426,16 +410,8 @@ func (c *Client) List(ctx context.Context, input ListInput) ([]apiClient.Query,
return nil, false, apierror.WrapChannelNotFound(
resp.JSON404, ErrListQueries, "channel ID "+input.ChannelID.String(),
)
case http.StatusUnauthorized:
c.logger.Error("Unauthorized when listing queries",
"status_code", resp.StatusCode(),
"body", string(resp.Body))
return nil, false, apierror.Wrap(resp.JSON401, ErrListQueries, resp.StatusCode())
default:
c.logger.Error("Unexpected status code when listing queries",
"status_code", resp.StatusCode(),
"body", string(resp.Body))
return nil, false, fmt.Errorf("%w: %w (status code %d)", ErrListQueries, apierror.ErrUnexpectedStatusCode, resp.StatusCode())
return nil, false, apierror.HandleErrorStatus(resp.StatusCode(), resp.JSON401, resp.JSON403, ErrListQueries, "listing queries", resp.Body, c.logger)
}
}

Expand Down
Loading
Loading