Skip to content

Commit 0c4374e

Browse files
remote capabilities errors are properly handled as caperrors (#21746)
* remote capabilities errors are properly handled as caperrors * lint * Update core/capabilities/remote/executable/request/client_request.go Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> --------- Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
1 parent 6630770 commit 0c4374e

3 files changed

Lines changed: 87 additions & 3 deletions

File tree

core/capabilities/remote/executable/request/client_request.go

Lines changed: 32 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,7 @@ import (
1616

1717
"github.com/smartcontractkit/chainlink-common/pkg/beholder"
1818
commoncap "github.com/smartcontractkit/chainlink-common/pkg/capabilities"
19+
caperrors "github.com/smartcontractkit/chainlink-common/pkg/capabilities/errors"
1920
"github.com/smartcontractkit/chainlink-common/pkg/capabilities/pb"
2021
"github.com/smartcontractkit/chainlink-common/pkg/logger"
2122
"github.com/smartcontractkit/chainlink-protos/workflows/go/events"
@@ -27,6 +28,32 @@ import (
2728
p2ptypes "github.com/smartcontractkit/chainlink/v2/core/services/p2p/types"
2829
)
2930

31+
// errRemoteCapabilityExecuteError preserves the legacy "TRANSPORT : ErrorMsg" string from the
32+
// remote executable client while wrapping a deserialized caperrors.Error so callers can
33+
// errors.As into caperrors.Error after RPC (see capability_executor metrics).
34+
type errRemoteCapabilityExecuteError struct {
35+
s string
36+
wrap caperrors.Error
37+
}
38+
39+
func (e *errRemoteCapabilityExecuteError) Error() string { return e.s }
40+
41+
func (e *errRemoteCapabilityExecuteError) Unwrap() error { return e.wrap }
42+
43+
func newRemoteCapabilityExecuteError(transportErr types.Error, errMsg string) error {
44+
return &errRemoteCapabilityExecuteError{
45+
s: fmt.Sprintf("%s : %s", transportErr, errMsg),
46+
wrap: caperrors.DeserializeErrorFromString(errMsg),
47+
}
48+
}
49+
50+
func newRemoteCapabilityExecuteErrorWithMessage(display string, errMsg string) error {
51+
return &errRemoteCapabilityExecuteError{
52+
s: display,
53+
wrap: caperrors.DeserializeErrorFromString(errMsg),
54+
}
55+
}
56+
3057
type clientResponse struct {
3158
Result []byte
3259
Err error
@@ -351,9 +378,12 @@ func (c *ClientRequest) OnMessage(_ context.Context, msg *types.MessageBody) err
351378
}
352379

353380
if c.errorCount[msg.ErrorMsg] == c.requiredIdenticalResponses {
354-
c.sendResponse(clientResponse{Err: fmt.Errorf("%s : %s", msg.Error, msg.ErrorMsg)})
381+
c.sendResponse(clientResponse{Err: newRemoteCapabilityExecuteError(msg.Error, msg.ErrorMsg)})
355382
} else if c.totalErrorCount == c.remoteNodeCount-c.requiredIdenticalResponses+1 {
356-
c.sendResponse(clientResponse{Err: fmt.Errorf("received %d errors, last error %s : %s", c.totalErrorCount, msg.Error, msg.ErrorMsg)})
383+
c.sendResponse(clientResponse{Err: newRemoteCapabilityExecuteErrorWithMessage(
384+
fmt.Sprintf("received %d errors, last error %s : %s", c.totalErrorCount, msg.Error, msg.ErrorMsg),
385+
msg.ErrorMsg,
386+
)})
357387
}
358388
}
359389
return nil

core/capabilities/remote/executable/request/client_request_test.go

Lines changed: 53 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,7 @@ import (
1414

1515
"github.com/smartcontractkit/chainlink-common/pkg/beholder/beholdertest"
1616
commoncap "github.com/smartcontractkit/chainlink-common/pkg/capabilities"
17+
caperrors "github.com/smartcontractkit/chainlink-common/pkg/capabilities/errors"
1718
"github.com/smartcontractkit/chainlink-common/pkg/capabilities/pb"
1819
"github.com/smartcontractkit/chainlink-protos/cre/go/values"
1920
"github.com/smartcontractkit/chainlink-protos/workflows/go/events"
@@ -229,6 +230,58 @@ func Test_ClientRequest_MessageValidation(t *testing.T) {
229230
response := <-req.ResponseChan()
230231

231232
assert.Equal(t, fmt.Sprintf("%s : %s", types.Error_INTERNAL_ERROR, assert.AnError.Error()), response.Err.Error())
233+
234+
var capErr caperrors.Error
235+
require.ErrorAs(t, response.Err, &capErr)
236+
assert.Equal(t, caperrors.OriginSystem, capErr.Origin(), "non-serialized ErrorMsg falls back to private system capability error")
237+
assert.Equal(t, caperrors.VisibilityPrivate, capErr.Visibility())
238+
assert.Equal(t, caperrors.Unknown, capErr.Code())
239+
})
240+
241+
t.Run("Error response with serialized caperrors unwraps correctly as usererror", func(t *testing.T) {
242+
ctx := t.Context()
243+
capabilityPeers, capDonInfo, capInfo := capabilityDon(t, 4, 1)
244+
245+
dispatcher := &clientRequestTestDispatcher{msgs: make(chan *types.MessageBody, 100)}
246+
req, err := request.NewClientExecuteRequest(ctx, logger.Test(t), capabilityRequest, capInfo,
247+
workflowDonInfo, dispatcher, 10*time.Minute, nil, "")
248+
require.NoError(t, err)
249+
defer req.Cancel(errors.New("test end"))
250+
251+
<-dispatcher.msgs
252+
<-dispatcher.msgs
253+
assert.Empty(t, dispatcher.msgs)
254+
255+
serialized := caperrors.NewPublicUserError(errors.New("rpc error: EVM error invalid argument"), caperrors.FailedPrecondition).SerializeToRemoteString()
256+
msgWithError := &types.MessageBody{
257+
CapabilityId: capInfo.ID,
258+
CapabilityDonId: capDonInfo.ID,
259+
CallerDonId: workflowDonInfo.ID,
260+
Method: types.MethodExecute,
261+
Payload: rawResponse,
262+
MessageId: []byte("messageID"),
263+
Error: types.Error_INTERNAL_ERROR,
264+
ErrorMsg: serialized,
265+
}
266+
267+
msgWithError.Sender = capabilityPeers[0][:]
268+
err = req.OnMessage(ctx, msgWithError)
269+
require.NoError(t, err)
270+
271+
msgWithError.Sender = capabilityPeers[1][:]
272+
err = req.OnMessage(ctx, msgWithError)
273+
require.NoError(t, err)
274+
275+
response := <-req.ResponseChan()
276+
277+
wantDisplay := fmt.Sprintf("%s : %s", types.Error_INTERNAL_ERROR, serialized)
278+
assert.Equal(t, wantDisplay, response.Err.Error(), "It should be equal to 'Public:User:FailedPrecondition:rpc error: EVM error invalid argument'")
279+
280+
var capErr caperrors.Error
281+
require.ErrorAs(t, response.Err, &capErr)
282+
assert.Equal(t, caperrors.OriginUser, capErr.Origin())
283+
assert.Equal(t, caperrors.VisibilityPublic, capErr.Visibility())
284+
assert.Equal(t, caperrors.FailedPrecondition, capErr.Code())
232285
})
233286

234287
t.Run("Send three messages with different errors", func(t *testing.T) {

core/services/workflows/v2/capability_executor.go

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -226,7 +226,8 @@ func (c *ExecutionHelper) callCapability(ctx context.Context, request *sdkpb.Cap
226226

227227
execLogger.Debugw("Capability execution failed", "err", err)
228228
_ = events.EmitCapabilityFinishedEvent(ctx, loggerLabels, c.WorkflowExecutionID, request.Id, meteringRef, store.StatusErrored, request.Method, err)
229-
c.metrics.With(platform.KeyCapabilityID, request.Id, platform.KeyCapabilityErrorCode, caperrors.Unknown.String()).IncrementCapabilityFailureCounter(ctx)
229+
// TODO shouldn't all capabilities *always* return a typed error, and if so shouldn't the following metric alert us there's a bug we need to fix?
230+
c.metrics.With(platform.KeyCapabilityID, request.Id, platform.KeyCapabilityErrorCode, "BUG").IncrementCapabilityFailureCounter(ctx)
230231
c.metrics.IncrementTotalWorkflowStepErrorsCounter(ctx)
231232
return nil, fmt.Errorf("failed to execute capability: %w", err)
232233
}

0 commit comments

Comments
 (0)