Skip to content

[Bug]: client and client/* packages do not handle context cancellation #24500

Open
@abicky

Description

@abicky

Is there an existing issue for this?

  • I have searched the existing issues

What happened?

In general, functions making network requests should be cancelled when the context they receive is cancelled, but the functions in the client and client/* packages do not handle context cancellation.

Here is a simple example:

ctx, cancel := context.WithCancel(context.Background())
cancel()

height, err := rpc.GetChainHeight(clientCtx.WithCmdContext(ctx))
if err == nil {
	fmt.Println("Height:", height)
} else {
	fmt.Fprintln(os.Stderr, "Failed to get chain height:", err)
}

In this example, I expect rpc.GetChainHeight to fail immediately because CmdContext is cancelled before it is called, but it returns a height without an error.

It seems that the functions in the client and client/* packages should use CmdContext in the same way github.com/cosmos/cosmos-sdk/x/auth/client package does.
cf. https://github.com/cosmos/cosmos-sdk/blob/v0.50.13/x/auth/client/tx.go#L65

Specifically, the following changes will make some of the functions handle context cancellation:

diff --git a/client/query.go b/client/query.go
index 29b99bb918..1933f768b5 100644
--- a/client/query.go
+++ b/client/query.go
@@ -1,7 +1,6 @@
 package client
 
 import (
-	"context"
 	"fmt"
 	"strings"
 
@@ -95,7 +94,7 @@ func (ctx Context) queryABCI(req abci.RequestQuery) (abci.ResponseQuery, error)
 		Prove:  req.Prove,
 	}
 
-	result, err := node.ABCIQueryWithOptions(context.Background(), req.Path, req.Data, opts)
+	result, err := node.ABCIQueryWithOptions(ctx.CmdContext, req.Path, req.Data, opts)
 	if err != nil {
 		return abci.ResponseQuery{}, err
 	}
diff --git a/client/rpc/block.go b/client/rpc/block.go
index d1b99d7229..93e5d166fe 100644
--- a/client/rpc/block.go
+++ b/client/rpc/block.go
@@ -1,7 +1,6 @@
 package rpc
 
 import (
-	"context"
 	"encoding/hex"
 	"fmt"
 	"time"
@@ -20,7 +19,7 @@ func GetChainHeight(clientCtx client.Context) (int64, error) {
 		return -1, err
 	}
 
-	status, err := node.Status(context.Background())
+	status, err := node.Status(clientCtx.CmdContext)
 	if err != nil {
 		return -1, err
 	}
@@ -53,7 +52,7 @@ func QueryBlocks(clientCtx client.Context, page, limit int, query, orderBy strin
 		return nil, err
 	}
 
-	resBlocks, err := node.BlockSearch(context.Background(), query, &page, &limit, orderBy)
+	resBlocks, err := node.BlockSearch(clientCtx.CmdContext, query, &page, &limit, orderBy)
 	if err != nil {
 		return nil, err
 	}
@@ -79,7 +78,7 @@ func GetBlockByHeight(clientCtx client.Context, height *int64) (*cmt.Block, erro
 	// header -> BlockchainInfo
 	// header, tx -> Block
 	// results -> BlockResults
-	resBlock, err := node.Block(context.Background(), height)
+	resBlock, err := node.Block(clientCtx.CmdContext, height)
 	if err != nil {
 		return nil, err
 	}
@@ -104,7 +103,7 @@ func GetBlockByHash(clientCtx client.Context, hashHexString string) (*cmt.Block,
 		return nil, err
 	}
 
-	resBlock, err := node.BlockByHash(context.Background(), hash)
+	resBlock, err := node.BlockByHash(clientCtx.CmdContext, hash)
 
 	if err != nil {
 		return nil, err

Cosmos SDK Version

0.50.13

How to reproduce?

package main

import (
	"context"
	"fmt"
	"log"
	"os"

	"cosmossdk.io/x/tx/signing"
	rpchttp "github.com/cometbft/cometbft/rpc/client/http"
	jsonrpcclient "github.com/cometbft/cometbft/rpc/jsonrpc/client"
	"github.com/cosmos/cosmos-sdk/client"
	"github.com/cosmos/cosmos-sdk/client/rpc"
	"github.com/cosmos/cosmos-sdk/codec"
	"github.com/cosmos/cosmos-sdk/codec/address"
	"github.com/cosmos/cosmos-sdk/codec/types"
	"github.com/cosmos/cosmos-sdk/std"
	sdk "github.com/cosmos/cosmos-sdk/types"
	querytypes "github.com/cosmos/cosmos-sdk/types/query"
	"github.com/cosmos/cosmos-sdk/x/auth/tx"
	authtypes "github.com/cosmos/cosmos-sdk/x/auth/types"
	banktypes "github.com/cosmos/cosmos-sdk/x/bank/types"
	"github.com/cosmos/gogoproto/proto"
)

func createClient(nodeURI string) (*rpchttp.HTTP, error) {
	httpClient, err := jsonrpcclient.DefaultHTTPClient(nodeURI)
	if err != nil {
		return nil, err
	}

	return rpchttp.NewWithClient(nodeURI, "/websocket", httpClient)
}

func createClientContext(c *rpchttp.HTTP) (client.Context, error) {
	interfaceRegistry, _ := types.NewInterfaceRegistryWithOptions(types.InterfaceRegistryOptions{
		ProtoFiles: proto.HybridResolver,
		SigningOptions: signing.Options{
			AddressCodec: address.Bech32Codec{
				Bech32Prefix: sdk.GetConfig().GetBech32AccountAddrPrefix(),
			},
			ValidatorAddressCodec: address.Bech32Codec{
				Bech32Prefix: sdk.GetConfig().GetBech32ValidatorAddrPrefix(),
			},
		},
	})
	appCodec := codec.NewProtoCodec(interfaceRegistry)
	legacyAmino := codec.NewLegacyAmino()
	txConfig := tx.NewTxConfig(appCodec, tx.DefaultSignModes)

	if err := interfaceRegistry.SigningContext().Validate(); err != nil {
		return client.Context{}, err
	}

	std.RegisterLegacyAminoCodec(legacyAmino)
	std.RegisterInterfaces(interfaceRegistry)

	clientCtx := client.Context{}.
		WithCodec(appCodec).
		WithInterfaceRegistry(interfaceRegistry).
		WithTxConfig(txConfig).
		WithLegacyAmino(legacyAmino).
		WithAccountRetriever(authtypes.AccountRetriever{}).
		WithClient(c)

	return clientCtx, nil
}

func getBalances(ctx client.Context, addr sdk.AccAddress) (sdk.Coins, error) {
	params := banktypes.NewQueryAllBalancesRequest(addr, &querytypes.PageRequest{
		Key:        []byte(""),
		Offset:     0,
		Limit:      1000,
		CountTotal: true,
	}, true)

	queryClient := banktypes.NewQueryClient(ctx)
	res, err := queryClient.AllBalances(ctx.CmdContext, params)
	if err != nil {
		return nil, err
	}

	return res.Balances, nil
}

func main() {
	nodeURI := "tcp://localhost:26657"
	rpcClient, err := createClient(nodeURI)
	if err != nil {
		log.Fatal(err)
	}

	clientCtx, err := createClientContext(rpcClient)
	if err != nil {
		log.Fatal(err)
	}

	ctx, cancel := context.WithCancel(context.Background())
	cancel()
	clientCtx = clientCtx.WithCmdContext(ctx)

	height, err := rpc.GetChainHeight(clientCtx)
	if err == nil {
		fmt.Println("Height:", height)
	} else {
		fmt.Fprintln(os.Stderr, "Failed to get chain height:", err)
	}

	block, err := rpc.GetBlockByHeight(clientCtx, &height)
	if err == nil {
		clientCtx.PrintProto(block)
	} else {
		fmt.Fprintln(os.Stderr, "Failed to get block:", err)
	}

	balances, err := getBalances(clientCtx, sdk.MustAccAddressFromBech32("cosmos1g8hssmdd8uwp7vy4xahrgd0hary9x4m0963zma"))
	if err == nil {
		fmt.Println("Balances:", balances)
	} else {
		fmt.Fprintln(os.Stderr, "Failed to get balances:", err)
	}
}

Here is the actual output:

Height: 5959
{"header":{"version":{"block":"11","app":"0"},"chain_id":"testchain","height":"5959","time":"2025-04-13T14:11:10.483716Z","last_block_id":{"hash":"rucetZznDC6FBJsHoOXl1v6Pa4eUptrn5uqa3ACb9I8=","part_set_header":{"total":1,"hash":"V21mZVCXDh4nIl4wRLo3j94whpm759/+1rDUfT7/QCE="}},"last_commit_hash":"jI7oxlEclPe6Z8AkkdMJeuiGGuDQlG3aNvwlpqbG7p0=","data_hash":"47DEQpj8HBSa+/TImW+5JCeuQeRkm5NMpJWZG3hSuFU=","validators_hash":"qXYhUdL+h20+ohzS8RXMJMhLXxeaWMPxoKV/fAlt88E=","next_validators_hash":"qXYhUdL+h20+ohzS8RXMJMhLXxeaWMPxoKV/fAlt88E=","consensus_hash":"BICRvH3cKD93v7+R1zxE2ljD34qcvIZ0Bdi389qtoi8=","app_hash":"NA6nF7eHR/GFarIecl8iHfkUgbMHXi/JZVxRAc9/Ia0=","last_results_hash":"47DEQpj8HBSa+/TImW+5JCeuQeRkm5NMpJWZG3hSuFU=","evidence_hash":"47DEQpj8HBSa+/TImW+5JCeuQeRkm5NMpJWZG3hSuFU=","proposer_address":"9L5eHsYA7jWAx6dsSF32fSwCY7M="},"data":{"txs":[]},"evidence":{"evidence":[]},"last_commit":{"height":"5958","round":0,"block_id":{"hash":"rucetZznDC6FBJsHoOXl1v6Pa4eUptrn5uqa3ACb9I8=","part_set_header":{"total":1,"hash":"V21mZVCXDh4nIl4wRLo3j94whpm759/+1rDUfT7/QCE="}},"signatures":[{"block_id_flag":"BLOCK_ID_FLAG_COMMIT","validator_address":"9L5eHsYA7jWAx6dsSF32fSwCY7M=","timestamp":"2025-04-13T14:11:10.483716Z","signature":"FirufuiIUKfo0pHPEaJ9JN1wuDagzBz90KJQdpmuZ3LTl1eQhCBpL9nN8A6SSZ1xYkVX/U/oAjMXvQCxBRjTDA=="}]}}
Balances: 9999999999999990000000000stake

And here is the expected output:

Failed to get chain height: post failed: Post "http://localhost:26657": context canceled
Failed to get block: post failed: Post "http://localhost:26657": context canceled
Failed to get balances: post failed: Post "http://localhost:26657": context canceled

Metadata

Metadata

Assignees

No one assigned

    Labels

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions