diff --git a/rpc/client.go b/rpc/client.go index c3114ef1d..2b0016db8 100644 --- a/rpc/client.go +++ b/rpc/client.go @@ -538,6 +538,13 @@ func (c *Client) Subscribe(ctx context.Context, namespace string, channel interf return op.sub, nil } +// SupportsSubscriptions reports whether subscriptions are supported by the client +// transport. When this returns false, Subscribe and related methods will return +// ErrNotificationsUnsupported. +func (c *Client) SupportsSubscriptions() bool { + return !c.isHTTP +} + func (c *Client) newMessage(method string, paramsIn ...interface{}) (*jsonrpcMessage, error) { msg := &jsonrpcMessage{Version: vsn, ID: c.nextID(), Method: method} if paramsIn != nil { // prevent sending "params":null diff --git a/rpc/errors.go b/rpc/errors.go index abb698af7..438aff218 100644 --- a/rpc/errors.go +++ b/rpc/errors.go @@ -58,12 +58,13 @@ var ( ) const ( - errcodeDefault = -32000 - errcodeNotificationsUnsupported = -32001 - errcodeTimeout = -32002 - errcodeResponseTooLarge = -32003 - errcodePanic = -32603 - errcodeMarshalError = -32603 + errcodeDefault = -32000 + errcodeTimeout = -32002 + errcodeResponseTooLarge = -32003 + errcodePanic = -32603 + errcodeMarshalError = -32603 + + legacyErrcodeNotificationsUnsupported = -32001 ) const ( @@ -80,6 +81,34 @@ func (e *methodNotFoundError) Error() string { return fmt.Sprintf("the method %s does not exist/is not available", e.method) } +type notificationsUnsupportedError struct{} + +func (e notificationsUnsupportedError) Error() string { + return "notifications not supported" +} + +func (e notificationsUnsupportedError) ErrorCode() int { return -32601 } + +// Is checks for equivalence to another error. Here we define that all errors with code +// -32601 (method not found) are equivalent to notificationsUnsupportedError. This is +// done to enable the following pattern: +// +// sub, err := client.Subscribe(...) +// if errors.Is(err, rpc.ErrNotificationsUnsupported) { +// // server doesn't support subscriptions +// } +func (e notificationsUnsupportedError) Is(other error) bool { + if other == (notificationsUnsupportedError{}) { + return true + } + rpcErr, ok := other.(Error) + if ok { + code := rpcErr.ErrorCode() + return code == -32601 || code == legacyErrcodeNotificationsUnsupported + } + return false +} + type subscriptionNotFoundError struct{ namespace, subscription string } func (e *subscriptionNotFoundError) ErrorCode() int { return -32601 } diff --git a/rpc/handler.go b/rpc/handler.go index 4f48c7931..f44e4d7b0 100644 --- a/rpc/handler.go +++ b/rpc/handler.go @@ -530,10 +530,7 @@ func (h *handler) handleCall(cp *callProc, msg *jsonrpcMessage) *jsonrpcMessage // handleSubscribe processes *_subscribe method calls. func (h *handler) handleSubscribe(cp *callProc, msg *jsonrpcMessage) *jsonrpcMessage { if !h.allowSubscribe { - return msg.errorResponse(&internalServerError{ - code: errcodeNotificationsUnsupported, - message: ErrNotificationsUnsupported.Error(), - }) + return msg.errorResponse(ErrNotificationsUnsupported) } // Subscription method name is first argument. diff --git a/rpc/subscription.go b/rpc/subscription.go index 334ead3ac..3231c2cee 100644 --- a/rpc/subscription.go +++ b/rpc/subscription.go @@ -32,8 +32,17 @@ import ( ) var ( - // ErrNotificationsUnsupported is returned when the connection doesn't support notifications - ErrNotificationsUnsupported = errors.New("notifications not supported") + // ErrNotificationsUnsupported is returned by the client when the connection doesn't + // support notifications. You can use this error value to check for subscription + // support like this: + // + // sub, err := client.EthSubscribe(ctx, channel, "newHeads", true) + // if errors.Is(err, rpc.ErrNotificationsUnsupported) { + // // Server does not support subscriptions, fall back to polling. + // } + // + ErrNotificationsUnsupported = notificationsUnsupportedError{} + // ErrSubscriptionNotFound is returned when the notification for the given id is not found ErrSubscriptionNotFound = errors.New("subscription not found") )