[v4] support error handling flow like gRPC (#158)
Some checks failed
sync / sync (push) Failing after 1h5m56s
test / test (push) Failing after 1h9m11s
coverage / build (push) Failing after 1h9m22s

* add status package with tests and integrate into response parsing
* improve unit-tests
* improve readme
This commit is contained in:
2025-09-30 12:28:39 +05:00
committed by GitHub
parent 9bb2f8cffa
commit 6209a03044
7 changed files with 308 additions and 26 deletions

View File

@@ -11,11 +11,13 @@ import (
"github.com/stretchr/testify/require"
jsoncodec "go.unistack.org/micro-codec-json/v4"
"go.unistack.org/micro/v4/client"
microerr "go.unistack.org/micro/v4/errors"
"go.unistack.org/micro/v4/metadata"
"google.golang.org/protobuf/proto"
httpcli "go.unistack.org/micro-client-http/v4"
pb "go.unistack.org/micro-client-http/v4/builder/proto"
"go.unistack.org/micro-client-http/v4/status"
)
func TestClient_Call_Get(t *testing.T) {
@@ -648,7 +650,7 @@ func TestClient_Call_Delete(t *testing.T) {
}
}
func TestClient_Call_ErrorsMap(t *testing.T) {
func TestClient_Call_APIError_WithErrorsMap(t *testing.T) {
type (
request = pb.Test_Client_Call_Request
response = pb.Test_Client_Call_Response
@@ -657,9 +659,9 @@ func TestClient_Call_ErrorsMap(t *testing.T) {
)
tests := []struct {
name string
serverMock func() *httptest.Server
expectedErr error
name string
serverMock func() *httptest.Server
expectedStatus *status.Status
}{
{
name: "default error",
@@ -699,7 +701,12 @@ func TestClient_Call_ErrorsMap(t *testing.T) {
require.NoError(t, err)
}))
},
expectedErr: &defaultError{Code: "default-error-code", Msg: "default-error-message"},
expectedStatus: status.New(http.StatusBadRequest).WithDetails(
&defaultError{
Code: "default-error-code",
Msg: "default-error-message",
},
),
},
{
name: "special error",
@@ -740,7 +747,13 @@ func TestClient_Call_ErrorsMap(t *testing.T) {
require.NoError(t, err)
}))
},
expectedErr: &specialError{Code: "special-error-code", Msg: "special-error-message", Warning: "special-error-warning"},
expectedStatus: status.New(http.StatusForbidden).WithDetails(
&specialError{
Code: "special-error-code",
Msg: "special-error-message",
Warning: "special-error-warning",
},
),
},
}
@@ -783,7 +796,11 @@ func TestClient_Call_ErrorsMap(t *testing.T) {
opts...,
)
require.Equal(t, tt.expectedErr.Error(), err.Error())
require.Error(t, err)
s, ok := status.FromError(err)
require.True(t, ok)
require.NotNil(t, s)
require.Equal(t, tt.expectedStatus, s)
require.Empty(t, rsp)
require.Equal(t, "application/json", respMetadata.GetJoined("Content-Type"))
@@ -792,6 +809,91 @@ func TestClient_Call_ErrorsMap(t *testing.T) {
}
}
func TestClient_Call_APIError_WithoutErrorsMap(t *testing.T) {
type (
request = pb.Test_Client_Call_Request
response = pb.Test_Client_Call_Response
)
serverMock := func() *httptest.Server {
return httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
// Validate request
require.Equal(t, "POST", r.Method)
require.Equal(t, "/user/products", r.URL.RequestURI())
require.Equal(t, "application/json", r.Header.Get("Content-Type"))
require.Equal(t, "Bearer token", r.Header.Get("Authorization"))
require.Equal(t, "My-Header-Value", r.Header.Get("My-Header"))
buf, err := io.ReadAll(r.Body)
require.NoError(t, err)
defer r.Body.Close()
c := jsoncodec.NewCodec()
req := &request{}
err = c.Unmarshal(buf, req)
require.NoError(t, err)
require.True(t, proto.Equal(&request{UserId: "123", OrderId: 456}, req))
// Return response
w.Header().Set("Content-Type", "application/json")
w.Header().Set("My-Header", "My-Header-Value")
w.WriteHeader(http.StatusConflict)
resp := map[string]interface{}{"message": "not-mapped-error"}
buf, err = c.Marshal(resp)
require.NoError(t, err)
_, err = w.Write(buf)
require.NoError(t, err)
}))
}
expectedStatus := status.New(http.StatusConflict)
server := serverMock()
defer server.Close()
httpClient := httpcli.NewClient(
client.Codec("application/json", jsoncodec.NewCodec()),
)
var (
ctx = metadata.NewOutgoingContext(
context.Background(),
metadata.Pairs("Authorization", "Bearer token", "My-Header", "My-Header-Value"),
)
req = &request{UserId: "123", OrderId: 456}
rsp = &response{}
respMetadata = metadata.Metadata{}
)
opts := []client.CallOption{
client.WithAddress(server.URL),
client.WithResponseMetadata(&respMetadata),
httpcli.Method(http.MethodPost),
httpcli.Path("/user/products"),
httpcli.Body("*"),
}
err := httpClient.Call(
ctx,
httpClient.NewRequest("test.service", "Test.Call", req),
rsp,
opts...,
)
require.Error(t, err)
s, ok := status.FromError(err)
require.True(t, ok)
require.NotNil(t, s)
require.Equal(t, expectedStatus, s)
require.Empty(t, rsp)
require.Equal(t, "application/json", respMetadata.GetJoined("Content-Type"))
require.Equal(t, "My-Header-Value", respMetadata.GetJoined("My-Header"))
}
func TestClient_Call_HeadersAndCookies(t *testing.T) {
type (
request = pb.Test_Client_Call_Request
@@ -1197,6 +1299,12 @@ func TestClient_Call_RequestTimeoutError(t *testing.T) {
opts...,
)
require.Error(t, err)
require.Equal(t, err, &microerr.Error{
ID: "go.micro.client",
Detail: "context deadline exceeded",
Status: "Request Timeout",
Code: http.StatusRequestTimeout,
})
}
func TestClient_Call_ContextDeadlineError(t *testing.T) {
@@ -1239,6 +1347,12 @@ func TestClient_Call_ContextDeadlineError(t *testing.T) {
opts...,
)
require.Error(t, err)
require.Equal(t, err, &microerr.Error{
ID: "go.micro.client",
Detail: "context deadline exceeded",
Status: "Request Timeout",
Code: http.StatusRequestTimeout,
})
}
func TestClient_Call_ContextCanceled(t *testing.T) {
@@ -1281,4 +1395,10 @@ func TestClient_Call_ContextCanceled(t *testing.T) {
opts...,
)
require.Error(t, err)
require.Equal(t, err, &microerr.Error{
ID: "go.micro.client",
Detail: "context canceled",
Status: "Request Timeout",
Code: http.StatusRequestTimeout,
})
}