diff --git a/api/http/errors.go b/api/http/errors.go index 3cc6688d8e..51231bdf78 100644 --- a/api/http/errors.go +++ b/api/http/errors.go @@ -12,18 +12,29 @@ package http import ( "context" - "errors" "fmt" "net/http" "os" "strings" + + "github.com/sourcenetwork/defradb/errors" ) var env = os.Getenv("DEFRA_ENV") +// Errors returnable from this package. +// +// This list is incomplete and undefined errors may also be returned. +// Errors returned from this package may be tested against these errors with errors.Is. var ( - errNoListener = errors.New("cannot serve with no listener") - errSchema = errors.New("base must start with the http or https scheme") + ErrNoListener = errors.New("cannot serve with no listener") + ErrSchema = errors.New("base must start with the http or https scheme") + ErrDatabaseNotAvailable = errors.New("no database available") + ErrFormNotSupported = errors.New("content type application/x-www-form-urlencoded not yet supported") + ErrBodyEmpty = errors.New("body cannot be empty") + ErrMissingGQLQuery = errors.New("missing GraphQL query") + ErrPeerIdUnavailable = errors.New("no peer ID available. P2P might be disabled") + ErrStreamingUnsupported = errors.New("streaming unsupported") ) // ErrorResponse is the GQL top level object holding error items for the response payload. diff --git a/api/http/handler.go b/api/http/handler.go index 375f9b56a1..6ab802df12 100644 --- a/api/http/handler.go +++ b/api/http/handler.go @@ -118,7 +118,7 @@ func sendJSON(ctx context.Context, rw http.ResponseWriter, v any, code int) { func dbFromContext(ctx context.Context) (client.DB, error) { db, ok := ctx.Value(ctxDB{}).(client.DB) if !ok { - return nil, errors.New("no database available") + return nil, ErrDatabaseNotAvailable } return db, nil diff --git a/api/http/handlerfuncs.go b/api/http/handlerfuncs.go index ab6b9a85ad..6b28e13d2e 100644 --- a/api/http/handlerfuncs.go +++ b/api/http/handlerfuncs.go @@ -111,7 +111,7 @@ func execGQLHandler(rw http.ResponseWriter, req *http.Request) { handleErr( req.Context(), rw, - errors.New("content type application/x-www-form-urlencoded not yet supported"), + ErrFormNotSupported, http.StatusBadRequest, ) return @@ -121,7 +121,7 @@ func execGQLHandler(rw http.ResponseWriter, req *http.Request) { default: if req.Body == nil { - handleErr(req.Context(), rw, errors.New("body cannot be empty"), http.StatusBadRequest) + handleErr(req.Context(), rw, ErrBodyEmpty, http.StatusBadRequest) return } body, err := io.ReadAll(req.Body) @@ -135,7 +135,7 @@ func execGQLHandler(rw http.ResponseWriter, req *http.Request) { // if at this point query is still empty, return an error if query == "" { - handleErr(req.Context(), rw, errors.New("missing GraphQL query"), http.StatusBadRequest) + handleErr(req.Context(), rw, ErrMissingGQLQuery, http.StatusBadRequest) return } @@ -251,7 +251,7 @@ func getBlockHandler(rw http.ResponseWriter, req *http.Request) { func peerIDHandler(rw http.ResponseWriter, req *http.Request) { peerID, ok := req.Context().Value(ctxPeerID{}).(string) if !ok || peerID == "" { - handleErr(req.Context(), rw, errors.New("no peer ID available. P2P might be disabled"), http.StatusNotFound) + handleErr(req.Context(), rw, ErrPeerIdUnavailable, http.StatusNotFound) return } @@ -268,7 +268,7 @@ func peerIDHandler(rw http.ResponseWriter, req *http.Request) { func subscriptionHandler(pub *events.Publisher[events.Update], rw http.ResponseWriter, req *http.Request) { flusher, ok := rw.(http.Flusher) if !ok { - handleErr(req.Context(), rw, errors.New("streaming unsupported"), http.StatusInternalServerError) + handleErr(req.Context(), rw, ErrStreamingUnsupported, http.StatusInternalServerError) return } diff --git a/api/http/router.go b/api/http/router.go index af20e85485..caea8b2ea9 100644 --- a/api/http/router.go +++ b/api/http/router.go @@ -68,7 +68,7 @@ func setRoutes(h *handler) *handler { // The base must start with a http or https. func JoinPaths(base string, paths ...string) (*url.URL, error) { if !strings.HasPrefix(base, "http") { - return nil, errSchema + return nil, ErrSchema } u, err := url.Parse(base) diff --git a/api/http/router_test.go b/api/http/router_test.go index 3eda472811..e43260ef43 100644 --- a/api/http/router_test.go +++ b/api/http/router_test.go @@ -27,12 +27,12 @@ func TestJoinPathsWithBase(t *testing.T) { func TestJoinPathsWithNoBase(t *testing.T) { _, err := JoinPaths("", BlocksPath, "cid_of_some_sort") - assert.ErrorIs(t, errSchema, err) + assert.ErrorIs(t, ErrSchema, err) } func TestJoinPathsWithBaseWithoutHttpPrefix(t *testing.T) { _, err := JoinPaths("localhost:9181", BlocksPath, "cid_of_some_sort") - assert.ErrorIs(t, errSchema, err) + assert.ErrorIs(t, ErrSchema, err) } func TestJoinPathsWithNoPaths(t *testing.T) { diff --git a/api/http/server.go b/api/http/server.go index 52fa35ed3b..04a96bdeca 100644 --- a/api/http/server.go +++ b/api/http/server.go @@ -260,7 +260,7 @@ func (s *Server) listenWithTLS(ctx context.Context) error { // Run calls Serve with the receiver's listener func (s *Server) Run(ctx context.Context) error { if s.listener == nil { - return errNoListener + return ErrNoListener } if s.certManager != nil { diff --git a/api/http/server_test.go b/api/http/server_test.go index 6abe27f346..a6e65d8f71 100644 --- a/api/http/server_test.go +++ b/api/http/server_test.go @@ -24,7 +24,7 @@ func TestNewServerAndRunWithoutListener(t *testing.T) { ctx := context.Background() s := NewServer(nil, WithAddress(":0")) if ok := assert.NotNil(t, s); ok { - assert.Equal(t, errNoListener, s.Run(ctx)) + assert.Equal(t, ErrNoListener, s.Run(ctx)) } } diff --git a/cli/addreplicator.go b/cli/addreplicator.go index 07dd489837..f5c196e434 100644 --- a/cli/addreplicator.go +++ b/cli/addreplicator.go @@ -18,7 +18,7 @@ import ( "google.golang.org/grpc" "google.golang.org/grpc/credentials/insecure" - "github.com/sourcenetwork/defradb/errors" + defraClient "github.com/sourcenetwork/defradb/client" "github.com/sourcenetwork/defradb/logging" netclient "github.com/sourcenetwork/defradb/net/api/client" ) @@ -33,12 +33,12 @@ for the p2p data sync system.`, if err := cmd.Usage(); err != nil { return err } - return errors.New("must specify two arguments: collection and peer") + return NewErrMissingArgs(len(args), 2) } collection := args[0] peerAddr, err := ma.NewMultiaddr(args[1]) if err != nil { - return errors.Wrap("could not parse peer address", err) + return defraClient.NewErrParsingFailed(err, "peer") } log.FeedbackInfo( @@ -52,12 +52,12 @@ for the p2p data sync system.`, cred := insecure.NewCredentials() client, err := netclient.NewClient(cfg.Net.RPCAddress, grpc.WithTransportCredentials(cred)) if err != nil { - return errors.Wrap("failed to create RPC client", err) + return NewErrFailedToCreateRPCClient(err) } rpcTimeoutDuration, err := cfg.Net.RPCTimeoutDuration() if err != nil { - return errors.Wrap("failed to parse RPC timeout duration", err) + return defraClient.NewErrParsingFailed(err, "RPC timeout duration") } ctx, cancel := context.WithTimeout(cmd.Context(), rpcTimeoutDuration) @@ -65,7 +65,7 @@ for the p2p data sync system.`, pid, err := client.AddReplicator(ctx, collection, peerAddr) if err != nil { - return errors.Wrap("failed to add replicator, request failed", err) + return NewErrFailedToAddReplicator(err) } log.FeedbackInfo(ctx, "Successfully added replicator", logging.NewKV("PID", pid)) return nil diff --git a/cli/blocks_get.go b/cli/blocks_get.go index ddfcc72125..26f6fa7a05 100644 --- a/cli/blocks_get.go +++ b/cli/blocks_get.go @@ -11,7 +11,6 @@ package cli import ( - "fmt" "io" "net/http" "os" @@ -19,7 +18,6 @@ import ( "github.com/spf13/cobra" httpapi "github.com/sourcenetwork/defradb/api/http" - "github.com/sourcenetwork/defradb/errors" ) var getCmd = &cobra.Command{ @@ -27,45 +25,45 @@ var getCmd = &cobra.Command{ Short: "Get a block by its CID from the blockstore.", RunE: func(cmd *cobra.Command, args []string) (err error) { if len(args) != 1 { - return errors.New("missing argument: CID") + return NewErrMissingArg("CID") } cid := args[0] endpoint, err := httpapi.JoinPaths(cfg.API.AddressToURL(), httpapi.BlocksPath, cid) if err != nil { - return errors.Wrap("failed to join endpoint", err) + return NewErrFailedToJoinEndpoint(err) } res, err := http.Get(endpoint.String()) if err != nil { - return errors.Wrap("failed to send get request", err) + return NewErrFailedToSendRequest(err) } defer func() { if e := res.Body.Close(); e != nil { - err = errors.Wrap(fmt.Sprintf("failed to read response body: %v", e.Error()), err) + err = NewErrFailedToReadResponseBody(err) } }() response, err := io.ReadAll(res.Body) if err != nil { - return errors.Wrap("failed to read response body", err) + return NewErrFailedToReadResponseBody(err) } stdout, err := os.Stdout.Stat() if err != nil { - return errors.Wrap("failed to stat stdout", err) + return NewErrFailedToStatStdOut(err) } if isFileInfoPipe(stdout) { cmd.Println(string(response)) } else { graphlErr, err := hasGraphQLErrors(response) if err != nil { - return errors.Wrap("failed to handle GraphQL errors", err) + return NewErrFailedToHandleGQLErrors(err) } indentedResult, err := indentJSON(response) if err != nil { - return errors.Wrap("failed to pretty print response", err) + return NewErrFailedToPrettyPrintResponse(err) } if graphlErr { log.FeedbackError(cmd.Context(), indentedResult) diff --git a/cli/errors.go b/cli/errors.go new file mode 100644 index 0000000000..4640c5e21c --- /dev/null +++ b/cli/errors.go @@ -0,0 +1,83 @@ +// Copyright 2022 Democratized Data Foundation +// +// Use of this software is governed by the Business Source License +// included in the file licenses/BSL.txt. +// +// As of the Change Date specified in that file, in accordance with +// the Business Source License, use of this software will be governed +// by the Apache License, Version 2.0, included in the file +// licenses/APL.txt. + +package cli + +import "github.com/sourcenetwork/defradb/errors" + +const ( + errMissingArg string = "missing arguement" + errMissingArgs string = "missing arguements" + errFailedToCreateRPCClient string = "failed to create RPC client" + errFailedToAddReplicator string = "failed to add replicator, request failed" + errFailedToJoinEndpoint string = "failed to join endpoint" + errFailedToSendRequest string = "failed to send request" + errFailedToReadResponseBody string = "failed to read response body" + errFailedToStatStdOut string = "failed to stat stdout" + errFailedToHandleGQLErrors string = "failed to handle GraphQL errors" + errFailedToPrettyPrintResponse string = "failed to pretty print response" +) + +// Errors returnable from this package. +// +// This list is incomplete and undefined errors may also be returned. +// Errors returned from this package may be tested against these errors with errors.Is. +var ( + ErrMissingArg = errors.New(errMissingArg) + ErrMissingArgs = errors.New(errMissingArgs) + ErrFailToWrapRPCClient = errors.New(errFailedToCreateRPCClient) + ErrFailedToAddReplicator = errors.New(errFailedToAddReplicator) + ErrFailedToJoinEndpoint = errors.New(errFailedToJoinEndpoint) + ErrFailedToSendRequest = errors.New(errFailedToSendRequest) + ErrFailedToReadResponseBody = errors.New(errFailedToReadResponseBody) + ErrFailedToStatStdOut = errors.New(errFailedToStatStdOut) + ErrFailedToHandleGQLErrors = errors.New(errFailedToHandleGQLErrors) + ErrFailedToPrettyPrintResponse = errors.New(errFailedToPrettyPrintResponse) +) + +func NewErrMissingArg(name string) error { + return errors.New(errMissingArg, errors.NewKV("Name", name)) +} + +func NewErrMissingArgs(count int, provided int) error { + return errors.New(errMissingArgs, errors.NewKV("Required", count), errors.NewKV("Provided", provided)) +} + +func NewErrFailedToCreateRPCClient(inner error) error { + return errors.Wrap(errFailedToCreateRPCClient, inner) +} + +func NewErrFailedToAddReplicator(inner error) error { + return errors.Wrap(errFailedToAddReplicator, inner) +} + +func NewErrFailedToJoinEndpoint(inner error) error { + return errors.Wrap(errFailedToJoinEndpoint, inner) +} + +func NewErrFailedToSendRequest(inner error) error { + return errors.Wrap(errFailedToSendRequest, inner) +} + +func NewErrFailedToReadResponseBody(inner error) error { + return errors.Wrap(errFailedToReadResponseBody, inner) +} + +func NewErrFailedToStatStdOut(inner error) error { + return errors.Wrap(errFailedToStatStdOut, inner) +} + +func NewErrFailedToHandleGQLErrors(inner error) error { + return errors.Wrap(errFailedToHandleGQLErrors, inner) +} + +func NewErrFailedToPrettyPrintResponse(inner error) error { + return errors.Wrap(errFailedToPrettyPrintResponse, inner) +} diff --git a/client/dockey.go b/client/dockey.go index 48ca326566..b769201db5 100644 --- a/client/dockey.go +++ b/client/dockey.go @@ -18,8 +18,6 @@ import ( "github.com/ipfs/go-cid" mbase "github.com/multiformats/go-multibase" uuid "github.com/satori/go.uuid" - - "github.com/sourcenetwork/defradb/errors" ) // DocKey versions. @@ -56,7 +54,7 @@ func NewDocKeyV0(dataCID cid.Cid) DocKey { func NewDocKeyFromString(key string) (DocKey, error) { parts := strings.SplitN(key, "-", 2) if len(parts) != 2 { - return DocKey{}, errors.New("malformed DocKey, missing either version or cid") + return DocKey{}, ErrMalformedDocKey } versionStr := parts[0] _, data, err := mbase.Decode(versionStr) @@ -69,7 +67,7 @@ func NewDocKeyFromString(key string) (DocKey, error) { return DocKey{}, err } if _, ok := ValidDocKeyVersions[uint16(version)]; !ok { - return DocKey{}, errors.New("invalid DocKey version") + return DocKey{}, ErrInvalidDocKeyVersion } uuid, err := uuid.FromString(parts[1]) diff --git a/client/document.go b/client/document.go index bd51724f68..8eb494dba1 100644 --- a/client/document.go +++ b/client/document.go @@ -12,15 +12,12 @@ package client import ( "encoding/json" - "fmt" "strings" "sync" "github.com/fxamacker/cbor/v2" "github.com/ipfs/go-cid" mh "github.com/multiformats/go-multihash" - - "github.com/sourcenetwork/defradb/errors" ) // This is the main implementation starting point for accessing the internal Document API @@ -89,7 +86,7 @@ func NewDocFromMap(data map[string]any) (*Document, error) { delete(data, "_key") // remove the key so it isn't parsed further kstr, ok := k.(string) if !ok { - return nil, errors.New("provided _key in document must be a string type") + return nil, NewErrUnexpectedType[string]("data[_key]", k) } if doc.key, err = NewDocKeyFromString(kstr); err != nil { return nil, err @@ -341,7 +338,7 @@ func (doc *Document) setAndParseType(field string, value any) error { } default: - return errors.New(fmt.Sprintf("Unhandled type in raw JSON: %v => %T", field, val)) + return NewErrUnhandledType(field, val) } return nil } diff --git a/client/errors.go b/client/errors.go index 6abfed0da8..8d36aa0a4d 100644 --- a/client/errors.go +++ b/client/errors.go @@ -10,11 +10,17 @@ package client -import "github.com/sourcenetwork/defradb/errors" +import ( + "fmt" + + "github.com/sourcenetwork/defradb/errors" +) const ( errFieldNotExist string = "The given field does not exist" errSelectOfNonGroupField string = "cannot select a non-group-by field at group-level" + errUnexpectedType string = "unexpected type" + errParsingFailed string = "failed to parse argument" ) // Errors returnable from this package. @@ -24,6 +30,8 @@ const ( var ( ErrFieldNotExist = errors.New(errFieldNotExist) ErrSelectOfNonGroupField = errors.New(errSelectOfNonGroupField) + ErrUnexpectedType = errors.New(errUnexpectedType) + ErrParsingFailed = errors.New(errParsingFailed) ErrFieldNotObject = errors.New("trying to access field on a non object type") ErrValueTypeMismatch = errors.New("value does not match indicated type") ErrIndexNotFound = errors.New("no index found for given ID") @@ -31,6 +39,8 @@ var ( ErrInvalidUpdateTarget = errors.New("the target document to update is of invalid type") ErrInvalidUpdater = errors.New("the updater of a document is of invalid type") ErrInvalidDeleteTarget = errors.New("the target document to delete is of invalid type") + ErrMalformedDocKey = errors.New("malformed DocKey, missing either version or cid") + ErrInvalidDocKeyVersion = errors.New("invalid DocKey version") ) func NewErrFieldNotExist(name string) error { @@ -40,3 +50,25 @@ func NewErrFieldNotExist(name string) error { func NewErrSelectOfNonGroupField(name string) error { return errors.New(errSelectOfNonGroupField, errors.NewKV("Field", name)) } + +func NewErrUnexpectedType[TExpected any](property string, actual any) error { + var expected TExpected + return errors.WithStack( + ErrUnexpectedType, + errors.NewKV("Property", property), + errors.NewKV("Expected", fmt.Sprintf("%T", expected)), + errors.NewKV("Actual", fmt.Sprintf("%T", actual)), + ) +} + +func NewErrUnhandledType(property string, actual any) error { + return errors.WithStack( + ErrUnexpectedType, + errors.NewKV("Property", property), + errors.NewKV("Actual", fmt.Sprintf("%T", actual)), + ) +} + +func NewErrParsingFailed(inner error, argumentName string) error { + return errors.Wrap(errParsingFailed, inner, errors.NewKV("Argument", argumentName)) +} diff --git a/db/errors_test.go b/client/errors_test.go similarity index 72% rename from db/errors_test.go rename to client/errors_test.go index 0fd8e793d4..0bdf474751 100644 --- a/db/errors_test.go +++ b/client/errors_test.go @@ -8,7 +8,7 @@ // by the Apache License, Version 2.0, included in the file // licenses/APL.txt. -package db +package client import ( "testing" @@ -18,6 +18,7 @@ import ( func TestNewUnexpectedType(t *testing.T) { someString := "defradb" - err := NewErrUnexpectedType[int](someString) - assert.Equal(t, err.Error(), "unexpected type. Expected: int, Actual: string") + someLocation := "foo" + err := NewErrUnexpectedType[int](someLocation, someString) + assert.Equal(t, err.Error(), "unexpected type. Property: foo, Expected: int, Actual: string") } diff --git a/core/errors.go b/core/errors.go index 7ae703b3a1..75184da0e9 100644 --- a/core/errors.go +++ b/core/errors.go @@ -10,7 +10,9 @@ package core -import "github.com/sourcenetwork/defradb/errors" +import ( + "github.com/sourcenetwork/defradb/errors" +) var ( ErrEmptyKey = errors.New("received empty key string") diff --git a/db/errors.go b/db/errors.go index a5c353eae1..8fae61da30 100644 --- a/db/errors.go +++ b/db/errors.go @@ -11,21 +11,9 @@ package db import ( - "fmt" - "github.com/sourcenetwork/defradb/errors" ) var ( ErrSubscriptionsNotAllowed = errors.New("server does not accept subscriptions") - ErrUnexpectedType = errors.New("unexpected type") ) - -func NewErrUnexpectedType[T any](actual any) error { - var expected T - return errors.WithStack( - ErrUnexpectedType, - errors.NewKV("Expected", fmt.Sprintf("%T", expected)), - errors.NewKV("Actual", fmt.Sprintf("%T", actual)), - ) -} diff --git a/db/subscriptions.go b/db/subscriptions.go index c555638cb9..6e184ae700 100644 --- a/db/subscriptions.go +++ b/db/subscriptions.go @@ -39,7 +39,7 @@ func (db *db) checkForClientSubsciptions(r *request.Request) ( return pub, subRequest, nil } - return nil, nil, NewErrUnexpectedType[request.ObjectSubscription](s) + return nil, nil, client.NewErrUnexpectedType[request.ObjectSubscription]("SubcriptionSelection", s) } return nil, nil, nil }