diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index 071650db..8d3501de 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -21,7 +21,7 @@ jobs: - name: Install ginkgo run: | - go install github.com/onsi/ginkgo/v2/ginkgo@latest + go install github.com/onsi/ginkgo/v2/ginkgo - name: Run Golang unit tests working-directory: ./tests/unit/ @@ -53,8 +53,7 @@ jobs: cache: true - name: Install ginkgo - working-directory: ./.. - run: go install github.com/onsi/ginkgo/v2/ginkgo@latest + run: go install github.com/onsi/ginkgo/v2/ginkgo - name: Run Ginkgo integration tests working-directory: ./tests/integration/rest @@ -63,7 +62,6 @@ jobs: - name: Show logs if: failure() - working-directory: ./docker/localnet run: docker compose -f tests/docker-compose-testing.yml logs --tail --follow - name: Upload integration tests result diff --git a/README.md b/README.md index 0a98b691..ff84a235 100644 --- a/README.md +++ b/README.md @@ -46,10 +46,11 @@ To configure the resolver, modify the values under the `environment` section of 5. **`TESTNET_ENDPOINT_FALLBACK`** : Fallback testnet endpoint with the same format as `TESTNET_ENDPOINT`. Used when primary endpoint is unavailable. 6. **`RESOLVER_LISTENER`**`: A string with address and port where the resolver listens for requests from clients. 7. **`LOG_LEVEL`**: `debug`/`warn`/`info`/`error` - to define the application log level. +8. **`GRPC_MAX_RECV_MSG_SIZE`**: Maximum gRPC response size (in bytes) the resolver will accept from a ledger node. Default is `16777216` (16MB). Raise this if a DID's resource collection grows large enough to exceed the default. #### gRPC Endpoints used by DID Resolver -Our DID Resolver uses the [Cosmos gRPC endpoint](https://docs.cosmos.network/main/core/grpc_rest) from `cheqd-node` to fetch data. Typically, this would be running on port `9090` on a `cheqd-node` instance. +Our DID Resolver uses the [Cosmos gRPC endpoint](https://docs.cosmos.network/sdk/latest/learn/concepts/cli-grpc-rest) from `cheqd-node` to fetch data. Typically, this would be running on port `9090` on a `cheqd-node` instance. You can either use [public gRPC endpoints for the cheqd network](https://cosmos.directory/cheqd/nodes) (such as the default ones mentioned above), or point it to your own `cheqd-node` instance by enabling gRPC in the `app.toml` configuration file on a node: diff --git a/docker/docker-compose.yml b/docker/docker-compose.yml index 6d58eabb..d4e9689b 100644 --- a/docker/docker-compose.yml +++ b/docker/docker-compose.yml @@ -36,5 +36,9 @@ services: # Logging level LOG_LEVEL: "warn" + # Maximum gRPC response size the resolver will accept from a ledger node, in bytes. + # Raise this if a DID's resource collection grows large enough to exceed the default. + GRPC_MAX_RECV_MSG_SIZE: "16777216" # 16MB + # Interface and port to listen on in the container RESOLVER_LISTENER: "0.0.0.0:8080" diff --git a/main.go b/main.go index 5f534233..a9e72cbc 100644 --- a/main.go +++ b/main.go @@ -26,7 +26,7 @@ func serve() { endpointManager := services.NewEndpointManager(config) // Services - ledgerService := services.NewLedgerService(endpointManager) + ledgerService := services.NewLedgerService(endpointManager, config.GRPCMaxRecvMsgSize) didService := services.NewDIDDocService(types.DID_METHOD, ledgerService) resourceService := services.NewResourceService(types.DID_METHOD, ledgerService) diff --git a/services/endpoint_manager.go b/services/endpoint_manager.go index 0b0fe2aa..b2307117 100644 --- a/services/endpoint_manager.go +++ b/services/endpoint_manager.go @@ -311,7 +311,7 @@ func (em *EndpointManager) markEndpointUnhealthy(endpointHealth *EndpointHealth) // performSingleHealthCheck performs a simple, fast health check func (em *EndpointManager) performSingleHealthCheck(endpoint *types.Endpoint) bool { - conn, err := openGRPCConnectionWithTimeout(endpoint.URL, endpoint.UseTls, em.healthTimeout) + conn, err := openGRPCConnectionWithTimeout(endpoint.URL, endpoint.UseTls, em.healthTimeout, em.config.GRPCMaxRecvMsgSize) if err != nil { log.Debug().Err(err).Msgf("Health check failed for endpoint %s: connection failed", endpoint.URL) return false diff --git a/services/ledger_service.go b/services/ledger_service.go index 5319d2db..e3c36e4b 100644 --- a/services/ledger_service.go +++ b/services/ledger_service.go @@ -16,7 +16,9 @@ import ( "github.com/cheqd/did-resolver/utils" "github.com/rs/zerolog/log" "google.golang.org/grpc" + "google.golang.org/grpc/codes" "google.golang.org/grpc/credentials/insecure" + "google.golang.org/grpc/status" ) const ( @@ -34,12 +36,14 @@ type LedgerServiceI interface { type LedgerService struct { ledgers map[string]types.Network // namespace -> endpoint with configs endpointManager *EndpointManager + maxRecvMsgSize int } -func NewLedgerService(endpointManager *EndpointManager) LedgerService { +func NewLedgerService(endpointManager *EndpointManager, maxRecvMsgSize int) LedgerService { ls := LedgerService{} ls.ledgers = make(map[string]types.Network) ls.endpointManager = endpointManager + ls.maxRecvMsgSize = maxRecvMsgSize return ls } @@ -110,7 +114,7 @@ func (ls LedgerService) QueryDIDDoc(did string, version string) (*didTypes.DidDo if version == "" { didDocResponse, grpcErr := client.DidDoc(context.Background(), &didTypes.QueryDidDocRequest{Id: did}) if grpcErr != nil { - return nil, types.NewNotFoundError(did, types.JSON, grpcErr, false) + return nil, mapGrpcError(did, grpcErr, false) } return didDocResponse.Value, nil @@ -118,7 +122,7 @@ func (ls LedgerService) QueryDIDDoc(did string, version string) (*didTypes.DidDo didDocResponse, grpcErr := client.DidDocVersion(context.Background(), &didTypes.QueryDidDocVersionRequest{Id: did, Version: version}) if grpcErr != nil { - return nil, types.NewNotFoundError(did, types.JSON, grpcErr, false) + return nil, mapGrpcError(did, grpcErr, false) } return didDocResponse.Value, nil @@ -144,7 +148,7 @@ func (ls LedgerService) QueryAllDidDocVersionsMetadata(did string) ([]*didTypes. didDocResponse, grpcErr := client.AllDidDocVersionsMetadata(context.Background(), &didTypes.QueryAllDidDocVersionsMetadataRequest{Id: did}) if grpcErr != nil { - return nil, types.NewNotFoundError(did, types.JSON, grpcErr, false) + return nil, mapGrpcError(did, grpcErr, false) } return didDocResponse.Versions, nil @@ -171,7 +175,7 @@ func (ls LedgerService) QueryResource(did string, resourceId string) (*resourceT resourceResponse, grpcErr := client.Resource(context.Background(), &resourceTypes.QueryResourceRequest{CollectionId: collectionId, Id: resourceId}) if grpcErr != nil { log.Error().Msgf("Resource not found %s", grpcErr.Error()) - return nil, types.NewNotFoundError(did, types.JSON, grpcErr, true) + return nil, mapGrpcError(did, grpcErr, true) } return resourceResponse.Resource, nil @@ -197,7 +201,7 @@ func (ls LedgerService) QueryCollectionResources(did string) ([]*resourceTypes.M client := resourceTypes.NewQueryClient(conn) resourceResponse, grpcErr := client.CollectionResources(context.Background(), &resourceTypes.QueryCollectionResourcesRequest{CollectionId: collectionId}) if grpcErr != nil { - return nil, types.NewNotFoundError(did, types.JSON, grpcErr, false) + return nil, mapGrpcError(did, grpcErr, false) } return resourceResponse.Resources, nil @@ -226,7 +230,7 @@ func (ls LedgerService) openGRPCConnection(endpoint types.Network) (conn *grpc.C } // Use shared utility function to eliminate code duplication - return openGRPCConnectionWithTimeout(endpoint.Endpoints[0].URL, endpoint.Endpoints[0].UseTls, endpoint.Endpoints[0].Timeout) + return openGRPCConnectionWithTimeout(endpoint.Endpoints[0].URL, endpoint.Endpoints[0].UseTls, endpoint.Endpoints[0].Timeout, ls.maxRecvMsgSize) } func mustCloseGRPCConnection(conn *grpc.ClientConn) { @@ -273,14 +277,16 @@ func (ls LedgerService) getOtherEndpoint(namespace string, currentNetwork *types } // openGRPCConnectionWithTimeout creates a gRPC connection with timeout -func openGRPCConnectionWithTimeout(endpoint string, useTls bool, timeout time.Duration) (*grpc.ClientConn, error) { +func openGRPCConnectionWithTimeout(endpoint string, useTls bool, timeout time.Duration, maxRecvMsgSize int) (*grpc.ClientConn, error) { // Dial options (credentials only). Connection readiness is verified by the subsequent RPC's context timeout. cred := grpc.WithTransportCredentials(insecure.NewCredentials()) if useTls { cred = grpc.WithTransportCredentials(credentials.NewTLS(&tls.Config{})) } - conn, err := grpc.NewClient(endpoint, cred) + maxRecv := grpc.WithDefaultCallOptions(grpc.MaxCallRecvMsgSize(maxRecvMsgSize)) + + conn, err := grpc.NewClient(endpoint, cred, maxRecv) if err != nil { log.Error().Err(err).Msgf("openGRPCConnection: connection failed") return nil, err @@ -289,3 +295,14 @@ func openGRPCConnectionWithTimeout(endpoint string, useTls bool, timeout time.Du log.Info().Msg("openGRPCConnection: opened") return conn, nil } + +// mapGrpcError distinguishes a client-side message-size overflow (ResourceExhausted) — +// which can only mean "the ledger response didn't fit," never "the DID doesn't exist" — +// from every other gRPC error, which keeps existing notFound behaviour unchanged. +func mapGrpcError(did string, grpcErr error, isDereferencing bool) *types.IdentityError { + if grpcStatus, ok := status.FromError(grpcErr); ok && grpcStatus.Code() == codes.ResourceExhausted { + log.Error().Err(grpcErr).Msgf("gRPC response exceeded max message size for DID: %s", did) + return types.NewInternalError(did, types.JSON, grpcErr, isDereferencing) + } + return types.NewNotFoundError(did, types.JSON, grpcErr, isDereferencing) +} diff --git a/types/config.go b/types/config.go index 9415ef65..ea6a5234 100644 --- a/types/config.go +++ b/types/config.go @@ -37,6 +37,7 @@ type RawConfig struct { EnableFallbackEndpoints bool `mapstructure:"ENABLE_FALLBACK_ENDPOINTS"` ResolverListener string `mapstructure:"RESOLVER_LISTENER"` LogLevel string `mapstructure:"LOG_LEVEL"` + GRPCMaxRecvMsgSize int `mapstructure:"GRPC_MAX_RECV_MSG_SIZE"` } type Config struct { @@ -44,6 +45,7 @@ type Config struct { EnableFallbackEndpoints bool ResolverListener string LogLevel string + GRPCMaxRecvMsgSize int } func (c *Config) MarshalJson() (string, error) { diff --git a/types/helper.go b/types/helper.go index 0fc0182b..f93fc0ac 100644 --- a/types/helper.go +++ b/types/helper.go @@ -71,6 +71,7 @@ func LoadConfig() (Config, error) { viper.SetDefault("ENABLE_FALLBACK_ENDPOINTS", false) viper.SetDefault("LOG_LEVEL", "") viper.SetDefault("RESOLVER_LISTENER", "") + viper.SetDefault("GRPC_MAX_RECV_MSG_SIZE", 16*1024*1024) // 16MB viper.AutomaticEnv() rawConf := &RawConfig{} @@ -95,6 +96,10 @@ func MustLoadConfig() Config { } func NewConfig(rawConfig RawConfig) (Config, error) { + if rawConfig.GRPCMaxRecvMsgSize <= 0 { + return Config{}, fmt.Errorf("GRPC_MAX_RECV_MSG_SIZE must be positive, got %d", rawConfig.GRPCMaxRecvMsgSize) + } + // Parse primary endpoints mainnetPrimary, err := ParseGRPCEndpoint(rawConfig.MainnetEndpoint) if err != nil { @@ -142,6 +147,7 @@ func NewConfig(rawConfig RawConfig) (Config, error) { EnableFallbackEndpoints: rawConfig.EnableFallbackEndpoints, ResolverListener: rawConfig.ResolverListener, LogLevel: rawConfig.LogLevel, + GRPCMaxRecvMsgSize: rawConfig.GRPCMaxRecvMsgSize, }, nil } @@ -194,6 +200,7 @@ func NewConfig(rawConfig RawConfig) (Config, error) { EnableFallbackEndpoints: rawConfig.EnableFallbackEndpoints, ResolverListener: rawConfig.ResolverListener, LogLevel: rawConfig.LogLevel, + GRPCMaxRecvMsgSize: rawConfig.GRPCMaxRecvMsgSize, }, nil }