From 3caca64c8a47579d672bfd8c3b3d728243791454 Mon Sep 17 00:00:00 2001 From: "Mark S. Lewis" Date: Tue, 23 Nov 2021 16:27:06 +0000 Subject: [PATCH] Refine Gateway gRPC error status codes Signed-off-by: Mark S. Lewis --- internal/pkg/gateway/api.go | 81 ++++++----- internal/pkg/gateway/api_test.go | 231 ++++++++++++++++++++----------- internal/pkg/gateway/apiutils.go | 16 ++- 3 files changed, 209 insertions(+), 119 deletions(-) diff --git a/internal/pkg/gateway/api.go b/internal/pkg/gateway/api.go index 6d1c4911429..4a8d17c25a0 100644 --- a/internal/pkg/gateway/api.go +++ b/internal/pkg/gateway/api.go @@ -9,6 +9,7 @@ package gateway import ( "context" "fmt" + "io" "math/rand" "strings" "sync" @@ -59,9 +60,9 @@ func (gs *Server) Evaluate(ctx context.Context, request *gp.EvaluateRequest) (*g plan, err := gs.registry.evaluator(channel, chaincodeID, targetOrgs) if err != nil { if transientProtected { - return nil, status.Errorf(codes.Unavailable, "no endorsers found in the gateway's organization; retry specifying target organization(s) to protect transient data: %s", err) + return nil, status.Errorf(codes.FailedPrecondition, "no endorsers found in the gateway's organization; retry specifying target organization(s) to protect transient data: %s", err) } - return nil, status.Errorf(codes.Unavailable, "%s", err) + return nil, status.Errorf(codes.FailedPrecondition, "%s", err) } endorser := plan.endorsers()[0] @@ -76,8 +77,8 @@ func (gs *Server) Evaluate(ctx context.Context, request *gp.EvaluateRequest) (*g ctx, cancel := context.WithTimeout(ctx, gs.options.EndorsementTimeout) defer cancel() pr, err := endorser.client.ProcessProposal(ctx, signedProposal) - success, message, retry, remove := gs.responseStatus(pr, err) - if success { + code, message, retry, remove := gs.responseStatus(pr, err) + if code == codes.OK { response = pr.Response // Prefer result from proposal response as Response.Payload is not required to be transaction result if result, err := getResultFromProposalResponse(pr); err == nil { @@ -94,10 +95,10 @@ func (gs *Server) Evaluate(ctx context.Context, request *gp.EvaluateRequest) (*g if retry { endorser = plan.nextPeerInGroup(endorser) } else { - done <- newRpcError(codes.Aborted, "evaluate call to endorser returned error: "+message, errDetails...) + done <- newRpcError(code, "evaluate call to endorser returned error: "+message, errDetails...) } if endorser == nil { - done <- newRpcError(codes.Aborted, "failed to evaluate transaction, see attached details for more info", errDetails...) + done <- newRpcError(code, "failed to evaluate transaction, see attached details for more info", errDetails...) } } }() @@ -167,7 +168,7 @@ func (gs *Server) Endorse(ctx context.Context, request *gp.EndorseRequest) (*gp. // Otherwise, just let discovery pick one. plan, err = gs.registry.endorsementPlan(channel, defaultInterest, nil) if err != nil { - return nil, status.Errorf(codes.Unavailable, "%s", err) + return nil, status.Errorf(codes.FailedPrecondition, "%s", err) } } firstEndorser := plan.endorsers()[0] @@ -185,9 +186,9 @@ func (gs *Server) Endorse(ctx context.Context, request *gp.EndorseRequest) (*gp. ctx, cancel := context.WithTimeout(ctx, gs.options.EndorsementTimeout) defer cancel() firstResponse, err = firstEndorser.client.ProcessProposal(ctx, signedProposal) - success, message, _, remove := gs.responseStatus(firstResponse, err) + code, message, _, remove := gs.responseStatus(firstResponse, err) - if !success { + if code != codes.OK { logger.Warnw("Endorse call to endorser failed", "channel", request.ChannelId, "txID", request.TransactionId, "endorserAddress", firstEndorser.endpointConfig.address, "endorserMspid", firstEndorser.endpointConfig.mspid, "error", message) errDetails = append(errDetails, errorDetail(firstEndorser.endpointConfig, message)) if remove { @@ -229,7 +230,7 @@ func (gs *Server) Endorse(ctx context.Context, request *gp.EndorseRequest) (*gp. // The preferred discovery layout will contain the firstEndorser's Org. plan, err = gs.registry.endorsementPlan(channel, interest, firstEndorser) if err != nil { - return nil, status.Errorf(codes.Unavailable, "%s", err) + return nil, status.Errorf(codes.FailedPrecondition, "%s", err) } // 6. Remove the gateway org's endorser, since we've already done that @@ -262,8 +263,8 @@ func (gs *Server) Endorse(ctx context.Context, request *gp.EndorseRequest) (*gp. // Ignore the retry flag returned by the following responseStatus call. Endorse will retry until all endorsement layouts have been exhausted. // It tries to get a successful endorsement from each org and minimise the changes of a rogue peer scuppering the transaction. // If an org is behaving badly, it can move on to a different layout. - success, message, _, remove := gs.responseStatus(response, err) - if success { + code, message, _, remove := gs.responseStatus(response, err) + if code == codes.OK { logger.Debugw("Endorse call to endorser returned success", "channel", request.ChannelId, "txID", request.TransactionId, "numEndorsers", len(endorsers), "endorserAddress", e.endpointConfig.address, "endorserMspid", e.endpointConfig.mspid, "status", response.Response.Status, "message", response.Response.Message) responseMessage := response.GetResponse() @@ -335,37 +336,37 @@ func (gs *Server) Endorse(ctx context.Context, request *gp.EndorseRequest) (*gp. // determines how the gateway should react (retry?, close connection?). // Uses the grpc canonical status error codes and their recommended actions. // Returns: -// - response successful (bool) +// - response status code, with codes.OK indicating success and other values indicating likely error type // - error message extracted from the err or generated from 500 proposal response (string) // - should the gateway retry (only the Evaluate() uses this) (bool) // - should the gateway close the connection and remove the peer from its registry (bool) -func (gs *Server) responseStatus(response *peer.ProposalResponse, err error) (success bool, message string, retry bool, remove bool) { +func (gs *Server) responseStatus(response *peer.ProposalResponse, err error) (statusCode codes.Code, message string, retry bool, remove bool) { if err != nil { if response == nil { // there is no ProposalResponse, so this must have been generated by grpc in response to an unavailable peer // - close the connection and retry on another - return false, err.Error(), true, true + return codes.Unavailable, err.Error(), true, true } // there is a response and an err, so it must have been from the unpackProposal() or preProcess() stages // preProcess does all the signature and ACL checking. In either case, no point retrying, or closing the connection (it's a client error) - return false, err.Error(), false, false + return codes.FailedPrecondition, err.Error(), false, false } if response.Response.Status < 200 || response.Response.Status >= 400 { if response.Payload == nil && response.Response.Status == 500 { // there's a error 500 response but no payload, so the response was generated in the peer rather than the chaincode if strings.HasSuffix(response.Response.Message, chaincode.ErrorStreamTerminated) { // chaincode container crashed probably. Close connection and retry on another peer - return false, response.Response.Message, true, true + return codes.Aborted, response.Response.Message, true, true } // some other error - retry on another peer - return false, response.Response.Message, true, false + return codes.Aborted, response.Response.Message, true, false } else { // otherwise it must be an error response generated by the chaincode - return false, fmt.Sprintf("chaincode response %d, %s", response.Response.Status, response.Response.Message), false, false + return codes.Unknown, fmt.Sprintf("chaincode response %d, %s", response.Response.Status, response.Response.Message), false, false } } // anything else is a success - return true, "", false, false + return codes.OK, "", false, false } // Submit will send the signed transaction to the ordering service. The response indicates whether the transaction was @@ -384,7 +385,7 @@ func (gs *Server) Submit(ctx context.Context, request *gp.SubmitRequest) (*gp.Su } orderers, err := gs.registry.orderers(request.ChannelId) if err != nil { - return nil, status.Errorf(codes.Unavailable, "%s", err) + return nil, status.Errorf(codes.FailedPrecondition, "%s", err) } if len(orderers) == 0 { @@ -400,34 +401,38 @@ func (gs *Server) Submit(ctx context.Context, request *gp.SubmitRequest) (*gp.Su if err == nil { return &gp.SubmitResponse{}, nil } + logger.Warnw("Error sending transaction to orderer", "txID", request.TransactionId, "endpoint", orderer.address, "err", err) errDetails = append(errDetails, errorDetail(orderer.endpointConfig, err.Error())) + + errStatus := toRpcStatus(err) + if errStatus.Code() != codes.Unavailable { + return nil, newRpcError(errStatus.Code(), errStatus.Message(), errDetails...) + } } - return nil, newRpcError(codes.Aborted, "no orderers could successfully process transaction", errDetails...) + return nil, newRpcError(codes.Unavailable, "no orderers could successfully process transaction", errDetails...) } func (gs *Server) broadcast(ctx context.Context, orderer *orderer, txn *common.Envelope) error { broadcast, err := orderer.client.Broadcast(ctx) if err != nil { - return fmt.Errorf("failed to create BroadcastClient: %w", err) + return err } + if err := broadcast.Send(txn); err != nil { - return fmt.Errorf("failed to send transaction to orderer: %w", err) + return err } response, err := broadcast.Recv() if err != nil { - return fmt.Errorf("failed to receive response from orderer: %w", err) + return err } - if response == nil { - return fmt.Errorf("received nil response from orderer") + if response.GetStatus() != common.Status_SUCCESS { + return status.Errorf(codes.Aborted, "received unsuccessful response from orderer: %s", common.Status_name[int32(response.GetStatus())]) } - if response.Status != common.Status_SUCCESS { - return fmt.Errorf("received unsuccessful response from orderer: %s", common.Status_name[int32(response.Status)]) - } return nil } @@ -458,7 +463,7 @@ func (gs *Server) CommitStatus(ctx context.Context, signedRequest *gp.SignedComm txStatus, err := gs.commitFinder.TransactionStatus(ctx, request.ChannelId, request.TransactionId) if err != nil { - return nil, toRpcError(err, codes.FailedPrecondition) + return nil, toRpcError(err, codes.Aborted) } response := &gp.CommitStatusResponse{ @@ -494,7 +499,7 @@ func (gs *Server) ChaincodeEvents(signedRequest *gp.SignedChaincodeEventsRequest ledger, err := gs.ledgerProvider.Ledger(request.GetChannelId()) if err != nil { - return status.Error(codes.InvalidArgument, err.Error()) + return status.Error(codes.NotFound, err.Error()) } startBlock, err := startBlockFromLedgerPosition(ledger, request.GetStartPosition()) @@ -504,7 +509,7 @@ func (gs *Server) ChaincodeEvents(signedRequest *gp.SignedChaincodeEventsRequest ledgerIter, err := ledger.GetBlocksIterator(startBlock) if err != nil { - return status.Error(codes.Unavailable, err.Error()) + return status.Error(codes.Aborted, err.Error()) } eventsIter := event.NewChaincodeEventsIterator(ledgerIter) @@ -513,7 +518,7 @@ func (gs *Server) ChaincodeEvents(signedRequest *gp.SignedChaincodeEventsRequest for { response, err := eventsIter.Next() if err != nil { - return status.Error(codes.Unavailable, err.Error()) + return status.Error(codes.Aborted, err.Error()) } var matchingEvents []*peer.ChaincodeEvent @@ -531,7 +536,11 @@ func (gs *Server) ChaincodeEvents(signedRequest *gp.SignedChaincodeEventsRequest response.Events = matchingEvents if err := stream.Send(response); err != nil { - return err // Likely stream closed by the client + if err == io.EOF { + // Stream closed by the client + return status.Error(codes.Canceled, err.Error()) + } + return err } } } @@ -548,7 +557,7 @@ func startBlockFromLedgerPosition(ledger ledger.Ledger, position *ab.SeekPositio ledgerInfo, err := ledger.GetBlockchainInfo() if err != nil { - return 0, status.Error(codes.Unavailable, err.Error()) + return 0, status.Error(codes.Aborted, err.Error()) } return ledgerInfo.GetHeight(), nil diff --git a/internal/pkg/gateway/api_test.go b/internal/pkg/gateway/api_test.go index cd7c37b967a..bc00e050170 100644 --- a/internal/pkg/gateway/api_test.go +++ b/internal/pkg/gateway/api_test.go @@ -9,6 +9,7 @@ package gateway import ( "context" "fmt" + "io" "testing" "time" @@ -133,6 +134,7 @@ type testDef struct { identity []byte localResponse string errString string + errCode codes.Code errDetails []*pb.ErrorDetail endpointDefinition *endpointDef endorsingOrgs []string @@ -198,7 +200,8 @@ func TestEvaluate(t *testing.T) { name: "no endorsers", plan: endorsementPlan{}, members: []networkMember{}, - errString: "rpc error: code = Unavailable desc = no peers available to evaluate chaincode test_chaincode in channel test_channel", + errCode: codes.FailedPrecondition, + errString: "no peers available to evaluate chaincode test_chaincode in channel test_channel", }, { name: "five endorsers, prefer local org", @@ -277,7 +280,8 @@ func TestEvaluate(t *testing.T) { {"id5", "peer4:11051", "msp3", 7}, }, transientData: map[string][]byte{"transient-key": []byte("transient-value")}, - errString: "rpc error: code = Unavailable desc = no endorsers found in the gateway's organization; retry specifying target organization(s) to protect transient data: no peers available to evaluate chaincode test_chaincode in channel test_channel", + errCode: codes.FailedPrecondition, + errString: "no endorsers found in the gateway's organization; retry specifying target organization(s) to protect transient data: no peers available to evaluate chaincode test_chaincode in channel test_channel", }, { name: "evaluate with transient data and target (non-local) orgs should select the highest block height peer", @@ -300,7 +304,8 @@ func TestEvaluate(t *testing.T) { endpointDefinition: &endpointDef{ proposalError: status.Error(codes.Aborted, "wibble"), }, - errString: "rpc error: code = Aborted desc = failed to evaluate transaction, see attached details for more info", + errCode: codes.Aborted, + errString: "failed to evaluate transaction, see attached details for more info", errDetails: []*pb.ErrorDetail{{ Address: "localhost:7051", MspId: "msp1", @@ -316,7 +321,8 @@ func TestEvaluate(t *testing.T) { proposalResponseStatus: 400, proposalResponseMessage: "Mock chaincode error", }, - errString: "rpc error: code = Aborted desc = evaluate call to endorser returned error: chaincode response 400, Mock chaincode error", + errCode: codes.Unknown, + errString: "evaluate call to endorser returned error: chaincode response 400, Mock chaincode error", errDetails: []*pb.ErrorDetail{{ Address: "peer1:8051", MspId: "msp1", @@ -362,7 +368,8 @@ func TestEvaluate(t *testing.T) { peer1Mock.client.(*mocks.EndorserClient).ProcessProposalReturns(createErrorResponse(t, 500, "bad peer1 endorser", nil), nil) }, endorsingOrgs: []string{"msp1"}, - errString: "rpc error: code = Aborted desc = failed to evaluate transaction, see attached details for more info", + errCode: codes.Aborted, + errString: "failed to evaluate transaction, see attached details for more info", errDetails: []*pb.ErrorDetail{ { Address: "localhost:7051", @@ -394,7 +401,8 @@ func TestEvaluate(t *testing.T) { def.localEndorser.ProcessProposalReturns(createErrorResponse(t, 500, "invalid signature", nil), fmt.Errorf("invalid signature")) }, endorsingOrgs: []string{"msp1"}, - errString: "rpc error: code = Aborted desc = evaluate call to endorser returned error: invalid signature", + errCode: codes.FailedPrecondition, // Code path could fail for reasons other than authentication + errString: "evaluate call to endorser returned error: invalid signature", errDetails: []*pb.ErrorDetail{ { Address: "localhost:7051", @@ -422,7 +430,8 @@ func TestEvaluate(t *testing.T) { peer1Mock.client.(*mocks.EndorserClient).ProcessProposalReturns(createErrorResponse(t, 500, "error in simulation: chaincode stream terminated", nil), nil) }, endorsingOrgs: []string{"msp1"}, - errString: "rpc error: code = Aborted desc = failed to evaluate transaction, see attached details for more info", + errCode: codes.Aborted, + errString: "failed to evaluate transaction, see attached details for more info", errDetails: []*pb.ErrorDetail{ { Address: "localhost:7051", @@ -449,7 +458,8 @@ func TestEvaluate(t *testing.T) { return nil, nil }) }, - errString: "rpc error: code = Unavailable desc = failed to create new connection: endorser not answering", + errCode: codes.Unavailable, + errString: "failed to create new connection: endorser not answering", }, { name: "discovery returns incomplete information - no Properties", @@ -459,7 +469,8 @@ func TestEvaluate(t *testing.T) { PKIid: []byte("ill-defined"), }}) }, - errString: "rpc error: code = Unavailable desc = no peers available to evaluate chaincode test_chaincode in channel test_channel", + errCode: codes.FailedPrecondition, + errString: "no peers available to evaluate chaincode test_chaincode in channel test_channel", }, { name: "context timeout during evaluate", @@ -476,7 +487,8 @@ func TestEvaluate(t *testing.T) { return createProposalResponse(t, peer1Mock.address, "mock_response", 200, ""), nil } }, - errString: "rpc error: code = DeadlineExceeded desc = evaluate timeout expired", + errCode: codes.DeadlineExceeded, + errString: "evaluate timeout expired", }, } for _, tt := range tests { @@ -485,9 +497,8 @@ func TestEvaluate(t *testing.T) { response, err := test.server.Evaluate(test.ctx, &pb.EvaluateRequest{ProposedTransaction: test.signedProposal, TargetOrganizations: tt.endorsingOrgs}) - if tt.errString != "" { - checkError(t, err, tt.errString, tt.errDetails) - require.Nil(t, response) + if checkError(t, &tt, err) { + require.Nil(t, response, "response on error") return } @@ -552,12 +563,14 @@ func TestEndorse(t *testing.T) { { name: "endorse with specified orgs, but fails to satisfy one org", endorsingOrgs: []string{"msp2", "msp4"}, - errString: "rpc error: code = Unavailable desc = failed to find any endorsing peers for org(s): msp4", + errCode: codes.Unavailable, + errString: "failed to find any endorsing peers for org(s): msp4", }, { name: "endorse with specified orgs, but fails to satisfy two orgs", endorsingOrgs: []string{"msp2", "msp4", "msp5"}, - errString: "rpc error: code = Unavailable desc = failed to find any endorsing peers for org(s): msp4, msp5", + errCode: codes.Unavailable, + errString: "failed to find any endorsing peers for org(s): msp4, msp5", }, { name: "endorse with multiple layouts - default choice first layout", @@ -646,7 +659,8 @@ func TestEndorse(t *testing.T) { peer3Mock.client.(*mocks.EndorserClient).ProcessProposalReturns(nil, status.Error(codes.Aborted, "bad peer3 endorser")) peer4Mock.client.(*mocks.EndorserClient).ProcessProposalReturns(nil, status.Error(codes.Aborted, "bad peer4 endorser")) }, - errString: "rpc error: code = Aborted desc = failed to collect enough transaction endorsements, see attached details for more info", + errCode: codes.Aborted, + errString: "failed to collect enough transaction endorsements, see attached details for more info", errDetails: []*pb.ErrorDetail{ {Address: "peer2:9051", MspId: "msp2", Message: "rpc error: code = Aborted desc = bad peer2 endorser"}, {Address: "peer3:10051", MspId: "msp2", Message: "rpc error: code = Aborted desc = bad peer3 endorser"}, @@ -712,7 +726,8 @@ func TestEndorse(t *testing.T) { {"id4", "peer4:11051", "msp3", 5}, }, transientData: map[string][]byte{"transient-key": []byte("transient-value")}, - errString: "rpc error: code = FailedPrecondition desc = no endorsers found in the gateway's organization; retry specifying endorsing organization(s) to protect transient data", + errCode: codes.FailedPrecondition, + errString: "no endorsers found in the gateway's organization; retry specifying endorsing organization(s) to protect transient data", }, { name: "extra endorsers with transient data", @@ -750,8 +765,9 @@ func TestEndorse(t *testing.T) { {"g1": 1, "g3": 1}, {"g2": 1, "g3": 1}, }, + errCode: codes.FailedPrecondition, // the following is a substring of the error message - the endpoints get listed in indeterminate order which would lead to flaky test - errString: "rpc error: code = Unavailable desc = failed to select a set of endorsers that satisfy the endorsement policy due to unavailability of peers", + errString: "failed to select a set of endorsers that satisfy the endorsement policy due to unavailability of peers", }, { name: "non-matching responses", @@ -760,7 +776,8 @@ func TestEndorse(t *testing.T) { "g2": {{endorser: peer2Mock, height: 5}}, // msp2 }, localResponse: "different_response", - errString: "rpc error: code = Aborted desc = failed to assemble transaction: ProposalResponsePayloads do not match (base64): 'EhQaEgjIARoNbW9ja19yZXNwb25zZQ==' vs 'EhkaFwjIARoSZGlmZmVyZW50X3Jlc3BvbnNl'", + errCode: codes.Aborted, + errString: "failed to assemble transaction: ProposalResponsePayloads do not match (base64): 'EhQaEgjIARoNbW9ja19yZXNwb25zZQ==' vs 'EhkaFwjIARoSZGlmZmVyZW50X3Jlc3BvbnNl'", }, { name: "discovery fails", @@ -770,7 +787,8 @@ func TestEndorse(t *testing.T) { postSetup: func(t *testing.T, def *preparedTest) { def.discovery.PeersForEndorsementReturns(nil, fmt.Errorf("peach-melba")) }, - errString: "rpc error: code = Unavailable desc = no combination of peers can be derived which satisfy the endorsement policy: peach-melba", + errCode: codes.FailedPrecondition, + errString: "no combination of peers can be derived which satisfy the endorsement policy: peach-melba", }, { name: "discovery returns incomplete protos - nil layout", @@ -784,7 +802,8 @@ func TestEndorse(t *testing.T) { } def.discovery.PeersForEndorsementReturns(ed, nil) }, - errString: "rpc error: code = Unavailable desc = failed to select a set of endorsers that satisfy the endorsement policy", + errCode: codes.FailedPrecondition, + errString: "failed to select a set of endorsers that satisfy the endorsement policy", }, { name: "discovery returns incomplete protos - nil state info", @@ -799,7 +818,8 @@ func TestEndorse(t *testing.T) { } def.discovery.PeersForEndorsementReturns(ed, nil) }, - errString: "rpc error: code = Unavailable desc = failed to select a set of endorsers that satisfy the endorsement policy", + errCode: codes.FailedPrecondition, + errString: "failed to select a set of endorsers that satisfy the endorsement policy", }, { name: "process proposal fails", @@ -809,7 +829,8 @@ func TestEndorse(t *testing.T) { endpointDefinition: &endpointDef{ proposalError: status.Error(codes.Aborted, "wibble"), }, - errString: "rpc error: code = Aborted desc = failed to endorse transaction, see attached details for more info", + errCode: codes.Aborted, + errString: "failed to endorse transaction, see attached details for more info", errDetails: []*pb.ErrorDetail{ { Address: "localhost:7051", @@ -835,7 +856,8 @@ func TestEndorse(t *testing.T) { postSetup: func(t *testing.T, def *preparedTest) { def.localEndorser.ProcessProposalReturns(createProposalResponse(t, localhostMock.address, "all_good", 200, ""), nil) }, - errString: "rpc error: code = Aborted desc = failed to collect enough transaction endorsements, see attached details for more info", + errCode: codes.Aborted, + errString: "failed to collect enough transaction endorsements, see attached details for more info", errDetails: []*pb.ErrorDetail{{ Address: "peer4:11051", MspId: "msp3", @@ -851,7 +873,8 @@ func TestEndorse(t *testing.T) { proposalResponseStatus: 400, proposalResponseMessage: "Mock chaincode error", }, - errString: "rpc error: code = Aborted desc = failed to endorse transaction, see attached details for more info", + errCode: codes.Aborted, + errString: "failed to endorse transaction, see attached details for more info", errDetails: []*pb.ErrorDetail{ { Address: "localhost:7051", @@ -878,7 +901,8 @@ func TestEndorse(t *testing.T) { postSetup: func(t *testing.T, def *preparedTest) { def.localEndorser.ProcessProposalReturns(createProposalResponse(t, localhostMock.address, "all_good", 200, ""), nil) }, - errString: "rpc error: code = Aborted desc = failed to collect enough transaction endorsements, see attached details for more info", + errCode: codes.Aborted, + errString: "failed to collect enough transaction endorsements, see attached details for more info", errDetails: []*pb.ErrorDetail{{ Address: "peer4:11051", MspId: "msp3", @@ -916,7 +940,8 @@ func TestEndorse(t *testing.T) { return createProposalResponse(t, peer1Mock.address, "mock_response", 200, ""), nil } }, - errString: "rpc error: code = DeadlineExceeded desc = endorsement timeout expired while collecting first endorsement", + errCode: codes.DeadlineExceeded, + errString: "endorsement timeout expired while collecting first endorsement", }, { name: "context timeout collecting endorsements", @@ -934,7 +959,8 @@ func TestEndorse(t *testing.T) { return createProposalResponse(t, peer4Mock.address, "mock_response", 200, ""), nil } }, - errString: "rpc error: code = DeadlineExceeded desc = endorsement timeout expired while collecting endorsements", + errCode: codes.DeadlineExceeded, + errString: "endorsement timeout expired while collecting endorsements", }, } for _, tt := range tests { @@ -943,9 +969,8 @@ func TestEndorse(t *testing.T) { response, err := test.server.Endorse(test.ctx, &pb.EndorseRequest{ProposedTransaction: test.signedProposal, EndorsingOrganizations: tt.endorsingOrgs}) - if tt.errString != "" { - checkError(t, err, tt.errString, tt.errDetails) - require.Nil(t, response) + if checkError(t, &tt, err) { + require.Nil(t, response, "response on error") return } @@ -983,7 +1008,8 @@ func TestSubmit(t *testing.T) { postSetup: func(t *testing.T, def *preparedTest) { def.discovery.ConfigReturnsOnCall(1, nil, fmt.Errorf("jabberwocky")) }, - errString: "rpc error: code = Unavailable desc = failed to get config for channel [test_channel]: jabberwocky", + errCode: codes.FailedPrecondition, + errString: "failed to get config for channel [test_channel]: jabberwocky", }, { name: "no orderers", @@ -996,7 +1022,8 @@ func TestSubmit(t *testing.T) { Msps: map[string]*msp.FabricMSPConfig{}, }, nil) }, - errString: "rpc error: code = Unavailable desc = no orderer nodes available", + errCode: codes.Unavailable, + errString: "no orderer nodes available", }, { name: "orderer broadcast fails", @@ -1007,11 +1034,12 @@ func TestSubmit(t *testing.T) { proposalResponseStatus: 200, ordererBroadcastError: status.Error(codes.FailedPrecondition, "Orderer not listening!"), }, - errString: "rpc error: code = Aborted desc = no orderers could successfully process transaction", + errCode: codes.FailedPrecondition, + errString: "Orderer not listening!", errDetails: []*pb.ErrorDetail{{ Address: "orderer:7050", MspId: "msp1", - Message: "failed to create BroadcastClient: rpc error: code = FailedPrecondition desc = Orderer not listening!", + Message: "rpc error: code = FailedPrecondition desc = Orderer not listening!", }}, }, { @@ -1023,11 +1051,12 @@ func TestSubmit(t *testing.T) { proposalResponseStatus: 200, ordererSendError: status.Error(codes.Internal, "Orderer says no!"), }, - errString: "rpc error: code = Aborted desc = no orderers could successfully process transaction", + errCode: codes.Internal, + errString: "Orderer says no!", errDetails: []*pb.ErrorDetail{{ Address: "orderer:7050", MspId: "msp1", - Message: "failed to send transaction to orderer: rpc error: code = Internal desc = Orderer says no!", + Message: "rpc error: code = Internal desc = Orderer says no!", }}, }, { @@ -1039,11 +1068,12 @@ func TestSubmit(t *testing.T) { proposalResponseStatus: 200, ordererRecvError: status.Error(codes.FailedPrecondition, "Orderer not happy!"), }, - errString: "rpc error: code = Aborted desc = no orderers could successfully process transaction", + errCode: codes.FailedPrecondition, + errString: "Orderer not happy!", errDetails: []*pb.ErrorDetail{{ Address: "orderer:7050", MspId: "msp1", - Message: "failed to receive response from orderer: rpc error: code = FailedPrecondition desc = Orderer not happy!", + Message: "rpc error: code = FailedPrecondition desc = Orderer not happy!", }}, }, { @@ -1060,11 +1090,12 @@ func TestSubmit(t *testing.T) { return abc } }, - errString: "rpc error: code = Aborted desc = no orderers could successfully process transaction", + errCode: codes.Aborted, + errString: "received unsuccessful response from orderer", errDetails: []*pb.ErrorDetail{{ Address: "orderer:7050", MspId: "msp1", - Message: "received nil response from orderer", + Message: "rpc error: code = Aborted desc = received unsuccessful response from orderer: " + cp.Status_name[int32(cp.Status_UNKNOWN)], }}, }, { @@ -1084,11 +1115,12 @@ func TestSubmit(t *testing.T) { return abc } }, - errString: "rpc error: code = Aborted desc = no orderers could successfully process transaction", + errCode: codes.Aborted, + errString: "received unsuccessful response from orderer: " + cp.Status_name[int32(cp.Status_BAD_REQUEST)], errDetails: []*pb.ErrorDetail{{ Address: "orderer:7050", MspId: "msp1", - Message: "received unsuccessful response from orderer: " + cp.Status_name[int32(cp.Status_BAD_REQUEST)], + Message: "rpc error: code = Aborted desc = received unsuccessful response from orderer: " + cp.Status_name[int32(cp.Status_BAD_REQUEST)], }}, }, { @@ -1104,7 +1136,8 @@ func TestSubmit(t *testing.T) { return nil, nil }) }, - errString: "rpc error: code = Unavailable desc = no orderer nodes available", + errCode: codes.Unavailable, + errString: "no orderer nodes available", }, { name: "orderer retry", @@ -1130,8 +1163,8 @@ func TestSubmit(t *testing.T) { postSetup: func(t *testing.T, def *preparedTest) { abc := &mocks.ABClient{} abbc := &mocks.ABBClient{} - abbc.SendReturnsOnCall(0, status.Error(codes.FailedPrecondition, "First orderer error")) - abbc.SendReturnsOnCall(1, status.Error(codes.FailedPrecondition, "Second orderer error")) + abbc.SendReturnsOnCall(0, status.Error(codes.Unavailable, "First orderer error")) + abbc.SendReturnsOnCall(1, status.Error(codes.Unavailable, "Second orderer error")) abbc.SendReturnsOnCall(2, nil) // third time lucky abbc.RecvReturns(&ab.BroadcastResponse{ Info: "success", @@ -1175,24 +1208,25 @@ func TestSubmit(t *testing.T) { }, endpointDefinition: &endpointDef{ proposalResponseStatus: 200, - ordererBroadcastError: status.Error(codes.FailedPrecondition, "Orderer not listening!"), + ordererBroadcastError: status.Error(codes.Unavailable, "Orderer not listening!"), }, - errString: "rpc error: code = Aborted desc = no orderers could successfully process transaction", + errCode: codes.Unavailable, + errString: "no orderers could successfully process transaction", errDetails: []*pb.ErrorDetail{ { Address: "orderer1:7050", MspId: "msp1", - Message: "failed to create BroadcastClient: rpc error: code = FailedPrecondition desc = Orderer not listening!", + Message: "rpc error: code = Unavailable desc = Orderer not listening!", }, { Address: "orderer2:7050", MspId: "msp1", - Message: "failed to create BroadcastClient: rpc error: code = FailedPrecondition desc = Orderer not listening!", + Message: "rpc error: code = Unavailable desc = Orderer not listening!", }, { Address: "orderer3:7050", MspId: "msp1", - Message: "failed to create BroadcastClient: rpc error: code = FailedPrecondition desc = Orderer not listening!", + Message: "rpc error: code = Unavailable desc = Orderer not listening!", }, }, }, @@ -1213,9 +1247,8 @@ func TestSubmit(t *testing.T) { // submit submitResponse, err := test.server.Submit(test.ctx, &pb.SubmitRequest{PreparedTransaction: preparedTx, ChannelId: testChannel}) - if tt.errString != "" { - checkError(t, err, tt.errString, tt.errDetails) - require.Nil(t, submitResponse) + if checkError(t, &tt, err) { + require.Nil(t, submitResponse, "response on error") return } @@ -1242,7 +1275,8 @@ func TestCommitStatus(t *testing.T) { { name: "error finding transaction status", finderErr: errors.New("FINDER_ERROR"), - errString: "rpc error: code = FailedPrecondition desc = FINDER_ERROR", + errCode: codes.Aborted, + errString: "FINDER_ERROR", }, { name: "returns transaction status", @@ -1284,7 +1318,8 @@ func TestCommitStatus(t *testing.T) { { name: "failed policy or signature check", policyErr: errors.New("POLICY_ERROR"), - errString: "rpc error: code = PermissionDenied desc = POLICY_ERROR", + errCode: codes.PermissionDenied, + errString: "POLICY_ERROR", }, { name: "passes channel name to policy checker", @@ -1318,12 +1353,14 @@ func TestCommitStatus(t *testing.T) { { name: "context timeout", finderErr: context.DeadlineExceeded, - errString: "rpc error: code = DeadlineExceeded desc = context deadline exceeded", + errCode: codes.DeadlineExceeded, + errString: "context deadline exceeded", }, { name: "context canceled", finderErr: context.Canceled, - errString: "rpc error: code = Canceled desc = context canceled", + errCode: codes.Canceled, + errString: "context canceled", }, } for _, tt := range tests { @@ -1345,9 +1382,8 @@ func TestCommitStatus(t *testing.T) { response, err := test.server.CommitStatus(test.ctx, signedRequest) - if tt.errString != "" { - checkError(t, err, tt.errString, tt.errDetails) - require.Nil(t, response) + if checkError(t, &tt, err) { + require.Nil(t, response, "response on error") return } @@ -1490,7 +1526,8 @@ func TestChaincodeEvents(t *testing.T) { { name: "error reading events", eventErr: errors.New("EVENT_ERROR"), - errString: "rpc error: code = Unavailable desc = EVENT_ERROR", + errCode: codes.Aborted, + errString: "EVENT_ERROR", }, { name: "returns chaincode events", @@ -1543,7 +1580,8 @@ func TestChaincodeEvents(t *testing.T) { blocks: []*cp.Block{ block101Proto, }, - errString: "rpc error: code = InvalidArgument desc = LEDGER_PROVIDER_ERROR", + errCode: codes.NotFound, + errString: "LEDGER_PROVIDER_ERROR", postSetup: func(t *testing.T, test *preparedTest) { test.ledgerProvider.LedgerReturns(nil, errors.New("LEDGER_PROVIDER_ERROR")) }, @@ -1553,7 +1591,8 @@ func TestChaincodeEvents(t *testing.T) { blocks: []*cp.Block{ block101Proto, }, - errString: "rpc error: code = Unavailable desc = LEDGER_INFO_ERROR", + errCode: codes.Aborted, + errString: "LEDGER_INFO_ERROR", postSetup: func(t *testing.T, test *preparedTest) { test.ledger.GetBlockchainInfoReturns(nil, errors.New("LEDGER_INFO_ERROR")) }, @@ -1628,24 +1667,37 @@ func TestChaincodeEvents(t *testing.T) { Oldest: &ab.SeekOldest{}, }, }, - errString: "rpc error: code = InvalidArgument desc = invalid start position type: *orderer.SeekPosition_Oldest", + errCode: codes.InvalidArgument, + errString: "invalid start position type: *orderer.SeekPosition_Oldest", }, { name: "returns error obtaining ledger iterator", blocks: []*cp.Block{ block101Proto, }, - errString: "rpc error: code = Unavailable desc = LEDGER_ITERATOR_ERROR", + errCode: codes.Aborted, + errString: "LEDGER_ITERATOR_ERROR", postSetup: func(t *testing.T, test *preparedTest) { test.ledger.GetBlocksIteratorReturns(nil, errors.New("LEDGER_ITERATOR_ERROR")) }, }, { - name: "returns error from send to client", + name: "returns canceled status error when client closes stream", + blocks: []*cp.Block{ + block101Proto, + }, + errCode: codes.Canceled, + postSetup: func(t *testing.T, test *preparedTest) { + test.eventsServer.SendReturns(io.EOF) + }, + }, + { + name: "returns status error from send to client", blocks: []*cp.Block{ block101Proto, }, - errString: "rpc error: code = Aborted desc = SEND_ERROR", + errCode: codes.Aborted, + errString: "SEND_ERROR", postSetup: func(t *testing.T, test *preparedTest) { test.eventsServer.SendReturns(status.Error(codes.Aborted, "SEND_ERROR")) }, @@ -1653,7 +1705,8 @@ func TestChaincodeEvents(t *testing.T) { { name: "failed policy or signature check", policyErr: errors.New("POLICY_ERROR"), - errString: "rpc error: code = PermissionDenied desc = POLICY_ERROR", + errCode: codes.PermissionDenied, + errString: "POLICY_ERROR", }, { name: "passes channel name to policy checker", @@ -1697,8 +1750,7 @@ func TestChaincodeEvents(t *testing.T) { err = test.server.ChaincodeEvents(signedRequest, test.eventsServer) - if tt.errString != "" { - checkError(t, err, tt.errString, tt.errDetails) + if checkError(t, &tt, err) { return } @@ -1890,14 +1942,39 @@ func prepareTest(t *testing.T, tt *testDef) *preparedTest { return pt } -func checkError(t *testing.T, err error, errString string, details []*pb.ErrorDetail) { - require.ErrorContains(t, err, errString) +func checkError(t *testing.T, tt *testDef, err error) (checked bool) { + stringCheck := tt.errString != "" + codeCheck := tt.errCode != codes.OK + detailsCheck := len(tt.errDetails) > 0 + + checked = stringCheck || codeCheck || detailsCheck + if !checked { + return + } + + require.NotNil(t, err, "error") + + if stringCheck { + require.ErrorContains(t, err, tt.errString, "error string") + } + s, ok := status.FromError(err) - require.True(t, ok, "Expected a gRPC status error") - require.Len(t, s.Details(), len(details)) - for _, detail := range s.Details() { - require.Contains(t, details, detail) + if !ok { + s = status.FromContextError(err) + } + + if codeCheck { + require.Equal(t, tt.errCode.String(), s.Code().String(), "error status code") } + + if detailsCheck { + require.Len(t, s.Details(), len(tt.errDetails)) + for _, detail := range s.Details() { + require.Contains(t, tt.errDetails, detail, "error details, expected: %v", tt.errDetails) + } + } + + return } func checkEndorsers(t *testing.T, endorsers []string, test *preparedTest) { diff --git a/internal/pkg/gateway/apiutils.go b/internal/pkg/gateway/apiutils.go index ea47ae85d90..d80bc23df54 100644 --- a/internal/pkg/gateway/apiutils.go +++ b/internal/pkg/gateway/apiutils.go @@ -63,12 +63,7 @@ func wrappedRpcError(err error, message string, details ...proto.Message) error } func toRpcError(err error, unknownCode codes.Code) error { - errStatus, ok := status.FromError(err) - if ok { - return errStatus.Err() - } - - errStatus = status.FromContextError(err) + errStatus := toRpcStatus(err) if errStatus.Code() != codes.Unknown { return errStatus.Err() } @@ -76,6 +71,15 @@ func toRpcError(err error, unknownCode codes.Code) error { return status.Error(unknownCode, err.Error()) } +func toRpcStatus(err error) *status.Status { + errStatus, ok := status.FromError(err) + if ok { + return errStatus + } + + return status.FromContextError(err) +} + func errorDetail(e *endpointConfig, msg string) *gp.ErrorDetail { return &gp.ErrorDetail{Address: e.address, MspId: e.mspid, Message: msg} }