diff --git a/server/router/api/v1/connect_handler.go b/server/router/api/v1/connect_handler.go index 95d90d7b..c77868fa 100644 --- a/server/router/api/v1/connect_handler.go +++ b/server/router/api/v1/connect_handler.go @@ -4,6 +4,7 @@ import ( "net/http" "connectrpc.com/connect" + "github.com/pkg/errors" "google.golang.org/grpc/codes" "google.golang.org/grpc/status" @@ -67,7 +68,7 @@ func convertGRPCError(err error) error { return nil } if st, ok := status.FromError(err); ok { - return connect.NewError(grpcCodeToConnectCode(st.Code()), err) + return connect.NewError(grpcCodeToConnectCode(st.Code()), errors.New(st.Message())) } return connect.NewError(connect.CodeInternal, err) } diff --git a/server/router/api/v1/connect_handler_test.go b/server/router/api/v1/connect_handler_test.go new file mode 100644 index 00000000..39a62ed4 --- /dev/null +++ b/server/router/api/v1/connect_handler_test.go @@ -0,0 +1,29 @@ +package v1 + +import ( + "strings" + "testing" + + "connectrpc.com/connect" + "google.golang.org/grpc/codes" + "google.golang.org/grpc/status" +) + +func TestConvertGRPCErrorUsesStatusMessage(t *testing.T) { + const message = "invalid username: must start with a lowercase letter" + + err := convertGRPCError(status.Error(codes.InvalidArgument, message)) + connectErr, ok := err.(*connect.Error) + if !ok { + t.Fatalf("convertGRPCError() returned %T, want *connect.Error", err) + } + if got := connectErr.Code(); got != connect.CodeInvalidArgument { + t.Fatalf("convertGRPCError() code = %v, want %v", got, connect.CodeInvalidArgument) + } + if got := connectErr.Message(); got != message { + t.Fatalf("convertGRPCError() message = %q, want %q", got, message) + } + if strings.Contains(connectErr.Message(), "rpc error:") { + t.Fatalf("convertGRPCError() message contains gRPC transport details: %q", connectErr.Message()) + } +} diff --git a/web/src/components/CreateUserDialog.tsx b/web/src/components/CreateUserDialog.tsx index 72b646e3..9cba53fc 100644 --- a/web/src/components/CreateUserDialog.tsx +++ b/web/src/components/CreateUserDialog.tsx @@ -74,7 +74,7 @@ function CreateUserDialog({ open, onOpenChange, user: initialUser, onSuccess }: onOpenChange(false); } catch (error: unknown) { handleError(error, toast.error, { - context: user ? "Update user" : "Create user", + context: isCreating ? "Create user" : "Update user", onError: () => requestState.setError(), }); } diff --git a/web/src/lib/error.ts b/web/src/lib/error.ts index ded8b495..dbebb8e4 100644 --- a/web/src/lib/error.ts +++ b/web/src/lib/error.ts @@ -1,4 +1,10 @@ +import { ConnectError } from "@connectrpc/connect"; + export function getErrorMessage(error: unknown, fallback = "Unknown error"): string { + if (error instanceof ConnectError) { + return error.rawMessage || fallback; + } + if (error instanceof Error) { return error.message; } diff --git a/web/tests/error.test.ts b/web/tests/error.test.ts new file mode 100644 index 00000000..262e92d1 --- /dev/null +++ b/web/tests/error.test.ts @@ -0,0 +1,17 @@ +import { Code, ConnectError } from "@connectrpc/connect"; +import { describe, expect, it } from "vitest"; +import { getErrorMessage } from "@/lib/error"; + +describe("getErrorMessage", () => { + it("returns a Connect error message without its status code", () => { + const error = new ConnectError("invalid username", Code.InvalidArgument); + + expect(getErrorMessage(error)).toBe("invalid username"); + }); + + it("uses the fallback for an empty Connect error message", () => { + const error = new ConnectError("", Code.InvalidArgument); + + expect(getErrorMessage(error, "Request failed")).toBe("Request failed"); + }); +});