Skip to content

Commit

Permalink
rpc: add method to test for subscription support (ethereum#25942)
Browse files Browse the repository at this point in the history
This adds two ways to check for subscription support. First, one can now check
whether the transport method (HTTP/WS/etc.) is capable of subscriptions using
the new Client.SupportsSubscriptions method.

Second, the error returned by Subscribe can now reliably be tested using this
pattern:
    
    sub, err := client.Subscribe(...)
    if errors.Is(err, rpc.ErrNotificationsUnsupported) {
        // no subscription support
    }

---------

Co-authored-by: Felix Lange <fjl@twurst.com>
  • Loading branch information
2 people authored and devopsbo3 committed Nov 10, 2023
1 parent 120956b commit e851e46
Show file tree
Hide file tree
Showing 4 changed files with 54 additions and 12 deletions.
7 changes: 7 additions & 0 deletions rpc/client.go
Expand Up @@ -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
Expand Down
41 changes: 35 additions & 6 deletions rpc/errors.go
Expand Up @@ -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 (
Expand All @@ -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 }
Expand Down
5 changes: 1 addition & 4 deletions rpc/handler.go
Expand Up @@ -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.
Expand Down
13 changes: 11 additions & 2 deletions rpc/subscription.go
Expand Up @@ -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")
)
Expand Down

0 comments on commit e851e46

Please sign in to comment.