Skip to content

Commit cd24d1a

Browse files
authored
feat: custom failures (#55)
This PR changes the server handler return types. Previously a handler signature was: ```go func[O any](Context, Capability[C], Invocation, InvocationContext) (O, fx.Effects, error) ``` It is now: ```go func[O any, X failure.IPLDBuilderFailure](Context, Capability[C], Invocation, InvocationContext) (result.Result[O, X], fx.Effects, error) ``` i.e. Instead of returning a success value (`O`), it now returns a `result.Result[O, X]`. This allows the handler to return a custom error in a `result.Result`. This is in the case where the error is _known_. Previously all errors would have a `name` of `HandlerExecutionError` and the actual error would be communicated in the `cause` property. This is awkward to unpack on the client, is redundant (every error is currently a `HandlerExecutionError`) and incompatible with the JS implementation. The intention of `HandlerExecutionError` is a catch all for _unexpected errors_, however it is currently being used to communicate _all_ failures. This is my fault for rushing the implementation. The failure type `X` is constrained to `failure.IPLDBuilderFailure`, which ensures known errors a) have a `name` property, b) are an `error` (and have a message) and c) have a `ToIPLD()` method. To summarise, handlers should communicate _known_ errors as an error result, and unexpected errors in the `error` return value. Note: there is an alternative to this #56
1 parent 9538ff6 commit cd24d1a

4 files changed

Lines changed: 118 additions & 30 deletions

File tree

server/handler.go

Lines changed: 22 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -14,13 +14,21 @@ import (
1414
"github.com/storacha/go-ucanto/validator"
1515
)
1616

17-
type HandlerFunc[C any, O ipld.Builder] func(ctx context.Context, capability ucan.Capability[C], invocation invocation.Invocation, context InvocationContext) (out O, fx fx.Effects, err error)
17+
type HandlerFunc[C any, O ipld.Builder, X failure.IPLDBuilderFailure] func(
18+
ctx context.Context,
19+
capability ucan.Capability[C],
20+
invocation invocation.Invocation,
21+
context InvocationContext,
22+
) (result result.Result[O, X], fx fx.Effects, err error)
1823

1924
// Provide is used to define given capability provider. It decorates the passed
2025
// handler and takes care of UCAN validation. It only calls the handler
2126
// when validation succeeds.
22-
func Provide[C any, O ipld.Builder](capability validator.CapabilityParser[C], handler HandlerFunc[C, O]) ServiceMethod[O] {
23-
return func(ctx context.Context, invocation invocation.Invocation, ictx InvocationContext) (transaction.Transaction[O, ipld.Builder], error) {
27+
func Provide[C any, O ipld.Builder, X failure.IPLDBuilderFailure](
28+
capability validator.CapabilityParser[C],
29+
handler HandlerFunc[C, O, X],
30+
) ServiceMethod[O, failure.IPLDBuilderFailure] {
31+
return func(ctx context.Context, invocation invocation.Invocation, ictx InvocationContext) (transaction.Transaction[O, failure.IPLDBuilderFailure], error) {
2432
vctx := validator.NewValidationContext(
2533
ictx.ID().Verifier(),
2634
capability,
@@ -46,19 +54,26 @@ func Provide[C any, O ipld.Builder](capability validator.CapabilityParser[C], ha
4654
if _, err := acceptedAudiences.Read(invocation.Audience().DID().String()); err != nil {
4755
expectedAudiences := append([]ucan.Principal{ictx.ID()}, ictx.AlternativeAudiences()...)
4856
audErr := NewInvalidAudienceError(invocation.Audience(), expectedAudiences...)
49-
return transaction.NewTransaction(result.Error[O, ipld.Builder](audErr)), nil
57+
return transaction.NewTransaction(result.Error[O, failure.IPLDBuilderFailure](audErr)), nil
5058
}
5159

5260
auth, aerr := validator.Access(ctx, invocation, vctx)
5361
if aerr != nil {
54-
return transaction.NewTransaction(result.Error[O, ipld.Builder](failure.FromError(aerr))), nil
62+
return transaction.NewTransaction(result.Error[O](failure.FromError(aerr))), nil
5563
}
5664

57-
o, fx, herr := handler(ctx, auth.Capability(), invocation, ictx)
65+
res, fx, herr := handler(ctx, auth.Capability(), invocation, ictx)
5866
if herr != nil {
5967
return nil, herr
6068
}
6169

62-
return transaction.NewTransaction(result.Ok[O, ipld.Builder](o), transaction.WithEffects(fx)), nil
70+
return transaction.NewTransaction(
71+
result.MapResultR0(
72+
res,
73+
func(o O) O { return o },
74+
func(x X) failure.IPLDBuilderFailure { return x },
75+
),
76+
transaction.WithEffects(fx),
77+
), nil
6378
}
6479
}

server/options.go

Lines changed: 8 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@ import (
77
"github.com/storacha/go-ucanto/core/invocation"
88
"github.com/storacha/go-ucanto/core/ipld"
99
"github.com/storacha/go-ucanto/core/result"
10+
"github.com/storacha/go-ucanto/core/result/failure"
1011
"github.com/storacha/go-ucanto/server/transaction"
1112
"github.com/storacha/go-ucanto/transport"
1213
"github.com/storacha/go-ucanto/ucan"
@@ -29,14 +30,18 @@ type srvConfig struct {
2930
catch ErrorHandlerFunc
3031
}
3132

32-
func WithServiceMethod[O ipld.Builder](can string, handleFunc ServiceMethod[O]) Option {
33+
func WithServiceMethod[O ipld.Builder, X failure.IPLDBuilderFailure](can string, handleFunc ServiceMethod[O, X]) Option {
3334
return func(cfg *srvConfig) error {
34-
cfg.service[can] = func(ctx context.Context, input invocation.Invocation, invCtx InvocationContext) (transaction.Transaction[ipld.Builder, ipld.Builder], error) {
35+
cfg.service[can] = func(ctx context.Context, input invocation.Invocation, invCtx InvocationContext) (transaction.Transaction[ipld.Builder, failure.IPLDBuilderFailure], error) {
3536
tx, err := handleFunc(ctx, input, invCtx)
3637
if err != nil {
3738
return nil, err
3839
}
39-
out := result.MapOk(tx.Out(), func(o O) ipld.Builder { return o })
40+
out := result.MapResultR0(
41+
tx.Out(),
42+
func(o O) ipld.Builder { return o },
43+
func(x X) failure.IPLDBuilderFailure { return x },
44+
)
4045
return transaction.NewTransaction(out, transaction.WithEffects(tx.Fx())), nil
4146
}
4247
return nil

server/server.go

Lines changed: 7 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,7 @@ import (
1717
"github.com/storacha/go-ucanto/core/message"
1818
"github.com/storacha/go-ucanto/core/receipt"
1919
"github.com/storacha/go-ucanto/core/result"
20+
"github.com/storacha/go-ucanto/core/result/failure"
2021
"github.com/storacha/go-ucanto/did"
2122
"github.com/storacha/go-ucanto/principal"
2223
"github.com/storacha/go-ucanto/principal/ed25519/verifier"
@@ -44,11 +45,15 @@ type InvocationContext interface {
4445
}
4546

4647
// ServiceMethod is an invocation handler.
47-
type ServiceMethod[O ipld.Builder] func(context.Context, invocation.Invocation, InvocationContext) (transaction.Transaction[O, ipld.Builder], error)
48+
type ServiceMethod[O ipld.Builder, X failure.IPLDBuilderFailure] func(
49+
context.Context,
50+
invocation.Invocation,
51+
InvocationContext,
52+
) (transaction.Transaction[O, X], error)
4853

4954
// Service is a mapping of service names to handlers, used to define a
5055
// service implementation.
51-
type Service = map[ucan.Ability]ServiceMethod[ipld.Builder]
56+
type Service = map[ucan.Ability]ServiceMethod[ipld.Builder, failure.IPLDBuilderFailure]
5257

5358
type ServiceInvocation = invocation.IssuedInvocation
5459

server/server_test.go

Lines changed: 81 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -74,6 +74,31 @@ func (ok uploadAddSuccess) ToIPLD() (ipld.Node, error) {
7474
return nb.Build(), nil
7575
}
7676

77+
type uploadAddFailure struct {
78+
name string
79+
message string
80+
}
81+
82+
func (x uploadAddFailure) Name() string {
83+
return x.name
84+
}
85+
86+
func (x uploadAddFailure) Error() string {
87+
return x.message
88+
}
89+
90+
func (x uploadAddFailure) ToIPLD() (ipld.Node, error) {
91+
np := basicnode.Prototype.Any
92+
nb := np.NewBuilder()
93+
ma, _ := nb.BeginMap(2)
94+
ma.AssembleKey().AssignString("name")
95+
ma.AssembleValue().AssignString(x.name)
96+
ma.AssembleKey().AssignString("message")
97+
ma.AssembleValue().AssignString(x.message)
98+
ma.Finish()
99+
return nb.Build(), nil
100+
}
101+
77102
var rcptsch = []byte(`
78103
type Result union {
79104
| UploadAddSuccess "ok"
@@ -129,8 +154,8 @@ func TestExecute(t *testing.T) {
129154
fixtures.Service,
130155
WithServiceMethod(
131156
uploadadd.Can(),
132-
Provide(uploadadd, func(ctx context.Context, cap ucan.Capability[uploadAddCaveats], inv invocation.Invocation, ictx InvocationContext) (uploadAddSuccess, fx.Effects, error) {
133-
return uploadAddSuccess{Root: cap.Nb().Root, Status: "done"}, nil, nil
157+
Provide(uploadadd, func(ctx context.Context, cap ucan.Capability[uploadAddCaveats], inv invocation.Invocation, ictx InvocationContext) (result.Result[uploadAddSuccess, uploadAddFailure], fx.Effects, error) {
158+
return result.Ok[uploadAddSuccess, uploadAddFailure](uploadAddSuccess{Root: cap.Nb().Root, Status: "done"}), nil, nil
134159
}),
135160
),
136161
))
@@ -152,13 +177,13 @@ func TestExecute(t *testing.T) {
152177
rcpt := helpers.Must(reader.Read(rcptlnk, resp.Blocks()))
153178

154179
result.MatchResultR0(rcpt.Out(), func(ok uploadAddSuccess) {
155-
fmt.Printf("%+v\n", ok)
180+
t.Logf("%+v\n", ok)
156181
require.Equal(t, ok.Root, rt)
157182
require.Equal(t, ok.Status, "done")
158183
}, func(x ipld.Node) {
159184
f := asFailure(t, x)
160-
fmt.Println(f.Message)
161-
fmt.Println(*f.Stack)
185+
t.Log(f.Message)
186+
t.Log(*f.Stack)
162187
require.Nil(t, f)
163188
})
164189
})
@@ -175,8 +200,8 @@ func TestExecute(t *testing.T) {
175200
fixtures.Service,
176201
WithServiceMethod(
177202
uploadadd.Can(),
178-
Provide(uploadadd, func(ctx context.Context, cap ucan.Capability[uploadAddCaveats], inv invocation.Invocation, ictx InvocationContext) (uploadAddSuccess, fx.Effects, error) {
179-
return uploadAddSuccess{Root: cap.Nb().Root, Status: "done"}, nil, nil
203+
Provide(uploadadd, func(ctx context.Context, cap ucan.Capability[uploadAddCaveats], inv invocation.Invocation, ictx InvocationContext) (result.Result[uploadAddSuccess, uploadAddFailure], fx.Effects, error) {
204+
return result.Ok[uploadAddSuccess, uploadAddFailure](uploadAddSuccess{Root: cap.Nb().Root, Status: "done"}), nil, nil
180205
}),
181206
),
182207
))
@@ -208,13 +233,13 @@ func TestExecute(t *testing.T) {
208233
rcpt := helpers.Must(reader.Read(rcptlnk, resp.Blocks()))
209234

210235
result.MatchResultR0(rcpt.Out(), func(ok uploadAddSuccess) {
211-
fmt.Printf("%+v\n", ok)
236+
t.Logf("%+v\n", ok)
212237
require.Equal(t, ok.Root, rt)
213238
require.Equal(t, ok.Status, "done")
214239
}, func(x ipld.Node) {
215240
f := asFailure(t, x)
216-
fmt.Println(f.Message)
217-
fmt.Println(*f.Stack)
241+
t.Log(f.Message)
242+
t.Log(*f.Stack)
218243
require.Nil(t, f)
219244
})
220245
})
@@ -242,7 +267,7 @@ func TestExecute(t *testing.T) {
242267
t.Fatalf("expected error: %s", invs[0].Link())
243268
}, func(x ipld.Node) {
244269
f := asFailure(t, x)
245-
fmt.Printf("%s %+v\n", *f.Name, f)
270+
t.Logf("%s %+v\n", *f.Name, f)
246271
require.Equal(t, *f.Name, "HandlerNotFoundError")
247272
})
248273
})
@@ -259,8 +284,8 @@ func TestExecute(t *testing.T) {
259284
fixtures.Service,
260285
WithServiceMethod(
261286
uploadadd.Can(),
262-
Provide(uploadadd, func(ctx context.Context, cap ucan.Capability[uploadAddCaveats], inv invocation.Invocation, ictx InvocationContext) (uploadAddSuccess, fx.Effects, error) {
263-
return uploadAddSuccess{}, nil, fmt.Errorf("test error")
287+
Provide(uploadadd, func(ctx context.Context, cap ucan.Capability[uploadAddCaveats], inv invocation.Invocation, ictx InvocationContext) (result.Result[uploadAddSuccess, uploadAddFailure], fx.Effects, error) {
288+
return nil, nil, fmt.Errorf("test error")
264289
}),
265290
),
266291
))
@@ -280,11 +305,49 @@ func TestExecute(t *testing.T) {
280305
t.Fatalf("expected error: %s", invs[0].Link())
281306
}, func(x ipld.Node) {
282307
f := asFailure(t, x)
283-
fmt.Printf("%s %+v\n", *f.Name, f)
308+
t.Logf("%s %+v\n", *f.Name, f)
284309
require.Equal(t, *f.Name, "HandlerExecutionError")
285310
})
286311
})
287312

313+
t.Run("failure", func(t *testing.T) {
314+
uploadadd := validator.NewCapability(
315+
"upload/add",
316+
schema.DIDString(),
317+
schema.Struct[uploadAddCaveats](uploadAddCaveatsType(), nil),
318+
nil,
319+
)
320+
321+
server := helpers.Must(NewServer(
322+
fixtures.Service,
323+
WithServiceMethod(
324+
uploadadd.Can(),
325+
Provide(uploadadd, func(ctx context.Context, cap ucan.Capability[uploadAddCaveats], inv invocation.Invocation, ictx InvocationContext) (result.Result[uploadAddSuccess, uploadAddFailure], fx.Effects, error) {
326+
return result.Error[uploadAddSuccess](uploadAddFailure{name: "UploadAddError", message: "boom"}), nil, nil
327+
}),
328+
),
329+
))
330+
331+
conn := helpers.Must(client.NewConnection(fixtures.Service, server))
332+
rt := cidlink.Link{Cid: cid.MustParse("bafkreiem4twkqzsq2aj4shbycd4yvoj2cx72vezicletlhi7dijjciqpui")}
333+
cap := uploadadd.New(fixtures.Alice.DID().String(), uploadAddCaveats{Root: rt})
334+
invs := []invocation.Invocation{helpers.Must(invocation.Invoke(fixtures.Alice, fixtures.Service, cap))}
335+
resp := helpers.Must(client.Execute(t.Context(), invs, conn))
336+
rcptlnk, ok := resp.Get(invs[0].Link())
337+
require.True(t, ok, "missing receipt for invocation: %s", invs[0].Link())
338+
339+
reader := helpers.Must(receipt.NewReceiptReader[uploadAddSuccess, ipld.Node](rcptsch))
340+
rcpt := helpers.Must(reader.Read(rcptlnk, resp.Blocks()))
341+
342+
result.MatchResultR0(rcpt.Out(), func(uploadAddSuccess) {
343+
t.Fatalf("expected error: %s", invs[0].Link())
344+
}, func(x ipld.Node) {
345+
f := asFailure(t, x)
346+
t.Logf("%s %+v\n", *f.Name, f)
347+
require.Equal(t, *f.Name, "UploadAddError")
348+
})
349+
})
350+
288351
t.Run("invalid audience", func(t *testing.T) {
289352
uploadadd := validator.NewCapability(
290353
"upload/add",
@@ -297,8 +360,8 @@ func TestExecute(t *testing.T) {
297360
fixtures.Service,
298361
WithServiceMethod(
299362
uploadadd.Can(),
300-
Provide(uploadadd, func(ctx context.Context, cap ucan.Capability[uploadAddCaveats], inv invocation.Invocation, ictx InvocationContext) (uploadAddSuccess, fx.Effects, error) {
301-
return uploadAddSuccess{Root: cap.Nb().Root, Status: "done"}, nil, nil
363+
Provide(uploadadd, func(ctx context.Context, cap ucan.Capability[uploadAddCaveats], inv invocation.Invocation, ictx InvocationContext) (result.Result[uploadAddSuccess, uploadAddFailure], fx.Effects, error) {
364+
return result.Ok[uploadAddSuccess, uploadAddFailure](uploadAddSuccess{Root: cap.Nb().Root, Status: "done"}), nil, nil
302365
}),
303366
),
304367
))
@@ -339,8 +402,8 @@ func TestExecute(t *testing.T) {
339402
fixtures.Service,
340403
WithServiceMethod(
341404
uploadadd.Can(),
342-
Provide(uploadadd, func(ctx context.Context, cap ucan.Capability[uploadAddCaveats], inv invocation.Invocation, ictx InvocationContext) (uploadAddSuccess, fx.Effects, error) {
343-
return uploadAddSuccess{Root: cap.Nb().Root, Status: "done"}, nil, nil
405+
Provide(uploadadd, func(ctx context.Context, cap ucan.Capability[uploadAddCaveats], inv invocation.Invocation, ictx InvocationContext) (result.Result[uploadAddSuccess, uploadAddFailure], fx.Effects, error) {
406+
return result.Ok[uploadAddSuccess, uploadAddFailure](uploadAddSuccess{Root: cap.Nb().Root, Status: "done"}), nil, nil
344407
}),
345408
),
346409
WithAlternativeAudiences(fixtures.Bob),

0 commit comments

Comments
 (0)