From ddb95d99b129005f980c47457410680b529e75ba Mon Sep 17 00:00:00 2001 From: Junyi Hou Date: Tue, 6 Jan 2026 16:41:22 +0800 Subject: [PATCH 1/6] feat: Add v2 RefreshToken endpoint and improve API client error message parsing and display. --- internal/api/actor.go | 13 ++-- internal/api/server.go | 14 +++- internal/libs/shared/error.go | 31 ++++++++- pkg/gen/api/auth/v1/auth.pb.go | 6 +- pkg/gen/api/auth/v1/auth.pb.gw.go | 66 +++++++++++++++++++ proto/auth/v1/auth.proto | 4 ++ webapp/_webapp/src/libs/apiclient.ts | 28 +++++++- .../src/pkg/gen/apiclient/auth/v1/auth_pb.ts | 2 +- 8 files changed, 150 insertions(+), 14 deletions(-) diff --git a/internal/api/actor.go b/internal/api/actor.go index 3e8f01eb..de41125c 100644 --- a/internal/api/actor.go +++ b/internal/api/actor.go @@ -3,35 +3,36 @@ package api import ( "context" - "go.mongodb.org/mongo-driver/v2/bson" "paperdebugger/internal/accesscontrol" "paperdebugger/internal/libs/jwt" "paperdebugger/internal/libs/shared" "paperdebugger/internal/services" + + "go.mongodb.org/mongo-driver/v2/bson" ) func parseUserActor(ctx context.Context, token string, userService *services.UserService) (*accesscontrol.Actor, error) { if len(token) == 0 { - return nil, shared.ErrInvalidToken() + return nil, shared.ErrInvalidToken("Authentication token is required") } claims, err := jwt.VerifyJwtToken(token) if err != nil { - return nil, shared.ErrInvalidToken(err) + return nil, shared.ErrInvalidToken("Token verification failed") } if len(claims.Audience) == 0 || claims.Audience[0] != "paperdebugger/user" { - return nil, shared.ErrInvalidActor() + return nil, shared.ErrInvalidActor("Invalid token audience") } actorID, err := bson.ObjectIDFromHex(claims.Subject) if err != nil { - return nil, shared.ErrInvalidActor() + return nil, shared.ErrInvalidActor("Invalid actor ID format") } _, err = userService.GetUserByID(ctx, actorID) if err != nil { - return nil, shared.ErrInvalidUser(err) + return nil, shared.ErrInvalidUser("User account not found") } return &accesscontrol.Actor{ID: actorID}, nil diff --git a/internal/api/server.go b/internal/api/server.go index 405ec61b..b093c767 100644 --- a/internal/api/server.go +++ b/internal/api/server.go @@ -166,7 +166,7 @@ func (s *Server) errorHandler() func(ctx context.Context, mux *runtime.ServeMux, err := &sharedv1.Error{ Code: errCode, - Message: reqError.Error(), + Message: cleanErrorMessage(reqError), } w.Header().Set("Content-Type", "application/json") w.WriteHeader(shared.GetHTTPCode(reqError)) @@ -182,3 +182,15 @@ func (s *Server) errorHandler() func(ctx context.Context, mux *runtime.ServeMux, } } } + +// cleanErrorMessage removes technical gRPC error prefixes from error messages +func cleanErrorMessage(err error) string { + msg := err.Error() + // Remove "rpc error: code = Code(XXXX) desc = " prefix + if strings.HasPrefix(msg, "rpc error:") { + if idx := strings.Index(msg, "desc = "); idx != -1 { + return msg[idx+7:] + } + } + return msg +} diff --git a/internal/libs/shared/error.go b/internal/libs/shared/error.go index 9d684963..1f1bc418 100644 --- a/internal/libs/shared/error.go +++ b/internal/libs/shared/error.go @@ -4,12 +4,28 @@ import ( "fmt" "net/http" + sharedv1 "paperdebugger/pkg/gen/api/shared/v1" + "github.com/samber/lo" "google.golang.org/grpc/codes" "google.golang.org/grpc/status" - sharedv1 "paperdebugger/pkg/gen/api/shared/v1" ) +// errorCodeMessages provides default user-friendly messages for each error code +var errorCodeMessages = map[sharedv1.ErrorCode]string{ + sharedv1.ErrorCode_ERROR_CODE_UNKNOWN: "An unknown error occurred", + sharedv1.ErrorCode_ERROR_CODE_INTERNAL: "Internal server error", + sharedv1.ErrorCode_ERROR_CODE_BAD_REQUEST: "Bad request", + sharedv1.ErrorCode_ERROR_CODE_INVALID_LLM_RESPONSE: "Invalid LLM response", + sharedv1.ErrorCode_ERROR_CODE_RECORD_NOT_FOUND: "Record not found", + sharedv1.ErrorCode_ERROR_CODE_INVALID_CREDENTIAL: "Invalid credentials", + sharedv1.ErrorCode_ERROR_CODE_INVALID_TOKEN: "Invalid or missing authentication token", + sharedv1.ErrorCode_ERROR_CODE_INVALID_ACTOR: "Invalid actor or session", + sharedv1.ErrorCode_ERROR_CODE_PERMISSION_DENIED: "Permission denied", + sharedv1.ErrorCode_ERROR_CODE_INVALID_USER: "User not found or invalid", + sharedv1.ErrorCode_ERROR_CODE_PROJECT_OUT_OF_DATE: "Project is out of date", +} + var ( ErrUnknown = makeErrorFunc(sharedv1.ErrorCode_ERROR_CODE_UNKNOWN) ErrInternal = makeErrorFunc(sharedv1.ErrorCode_ERROR_CODE_INTERNAL) @@ -59,6 +75,11 @@ func makeErrorFunc( detail := lo.FirstOrEmpty(details) var errorMessage string switch v := detail.(type) { + case nil: + // Use default message when no detail is provided + if defaultMsg, ok := errorCodeMessages[errorCode]; ok { + errorMessage = defaultMsg + } case error: errorMessage = v.Error() case interface{ String() string }: @@ -66,6 +87,14 @@ func makeErrorFunc( default: errorMessage = fmt.Sprintf("%v", v) } + // If still empty, use a generic message + if errorMessage == "" || errorMessage == "" { + if defaultMsg, ok := errorCodeMessages[errorCode]; ok { + errorMessage = defaultMsg + } else { + errorMessage = "An error occurred" + } + } return status.Error(codes.Code(errorCode), errorMessage) } } diff --git a/pkg/gen/api/auth/v1/auth.pb.go b/pkg/gen/api/auth/v1/auth.pb.go index 569ea4e8..0c903a0c 100644 --- a/pkg/gen/api/auth/v1/auth.pb.go +++ b/pkg/gen/api/auth/v1/auth.pb.go @@ -412,11 +412,11 @@ const file_auth_v1_auth_proto_rawDesc = "" + "\rrefresh_token\x18\x02 \x01(\tR\frefreshToken\"4\n" + "\rLogoutRequest\x12#\n" + "\rrefresh_token\x18\x01 \x01(\tR\frefreshToken\"\x10\n" + - "\x0eLogoutResponse2\xdb\x03\n" + + "\x0eLogoutResponse2\xfb\x03\n" + "\vAuthService\x12x\n" + "\rLoginByGoogle\x12\x1d.auth.v1.LoginByGoogleRequest\x1a\x1e.auth.v1.LoginByGoogleResponse\"(\x82\xd3\xe4\x93\x02\":\x01*\"\x1d/_pd/api/v1/auth/login/google\x12\x80\x01\n" + - "\x0fLoginByOverleaf\x12\x1f.auth.v1.LoginByOverleafRequest\x1a .auth.v1.LoginByOverleafResponse\"*\x82\xd3\xe4\x93\x02$:\x01*\"\x1f/_pd/api/v1/auth/login/overleaf\x12p\n" + - "\fRefreshToken\x12\x1c.auth.v1.RefreshTokenRequest\x1a\x1d.auth.v1.RefreshTokenResponse\"#\x82\xd3\xe4\x93\x02\x1d:\x01*\"\x18/_pd/api/v1/auth/refresh\x12]\n" + + "\x0fLoginByOverleaf\x12\x1f.auth.v1.LoginByOverleafRequest\x1a .auth.v1.LoginByOverleafResponse\"*\x82\xd3\xe4\x93\x02$:\x01*\"\x1f/_pd/api/v1/auth/login/overleaf\x12\x8f\x01\n" + + "\fRefreshToken\x12\x1c.auth.v1.RefreshTokenRequest\x1a\x1d.auth.v1.RefreshTokenResponse\"B\x82\xd3\xe4\x93\x02<:\x01*Z\x1d:\x01*\"\x18/_pd/api/v2/auth/refresh\"\x18/_pd/api/v1/auth/refresh\x12]\n" + "\x06Logout\x12\x16.auth.v1.LogoutRequest\x1a\x17.auth.v1.LogoutResponse\"\"\x82\xd3\xe4\x93\x02\x1c:\x01*\"\x17/_pd/api/v1/auth/logoutB\x7f\n" + "\vcom.auth.v1B\tAuthProtoP\x01Z(paperdebugger/pkg/gen/api/auth/v1;authv1\xa2\x02\x03AXX\xaa\x02\aAuth.V1\xca\x02\aAuth\\V1\xe2\x02\x13Auth\\V1\\GPBMetadata\xea\x02\bAuth::V1b\x06proto3" diff --git a/pkg/gen/api/auth/v1/auth.pb.gw.go b/pkg/gen/api/auth/v1/auth.pb.gw.go index b1b1200e..d4f74210 100644 --- a/pkg/gen/api/auth/v1/auth.pb.gw.go +++ b/pkg/gen/api/auth/v1/auth.pb.gw.go @@ -116,6 +116,33 @@ func local_request_AuthService_RefreshToken_0(ctx context.Context, marshaler run return msg, metadata, err } +func request_AuthService_RefreshToken_1(ctx context.Context, marshaler runtime.Marshaler, client AuthServiceClient, req *http.Request, pathParams map[string]string) (proto.Message, runtime.ServerMetadata, error) { + var ( + protoReq RefreshTokenRequest + metadata runtime.ServerMetadata + ) + if err := marshaler.NewDecoder(req.Body).Decode(&protoReq); err != nil && !errors.Is(err, io.EOF) { + return nil, metadata, status.Errorf(codes.InvalidArgument, "%v", err) + } + if req.Body != nil { + _, _ = io.Copy(io.Discard, req.Body) + } + msg, err := client.RefreshToken(ctx, &protoReq, grpc.Header(&metadata.HeaderMD), grpc.Trailer(&metadata.TrailerMD)) + return msg, metadata, err +} + +func local_request_AuthService_RefreshToken_1(ctx context.Context, marshaler runtime.Marshaler, server AuthServiceServer, req *http.Request, pathParams map[string]string) (proto.Message, runtime.ServerMetadata, error) { + var ( + protoReq RefreshTokenRequest + metadata runtime.ServerMetadata + ) + if err := marshaler.NewDecoder(req.Body).Decode(&protoReq); err != nil && !errors.Is(err, io.EOF) { + return nil, metadata, status.Errorf(codes.InvalidArgument, "%v", err) + } + msg, err := server.RefreshToken(ctx, &protoReq) + return msg, metadata, err +} + func request_AuthService_Logout_0(ctx context.Context, marshaler runtime.Marshaler, client AuthServiceClient, req *http.Request, pathParams map[string]string) (proto.Message, runtime.ServerMetadata, error) { var ( protoReq LogoutRequest @@ -209,6 +236,26 @@ func RegisterAuthServiceHandlerServer(ctx context.Context, mux *runtime.ServeMux } forward_AuthService_RefreshToken_0(annotatedContext, mux, outboundMarshaler, w, req, resp, mux.GetForwardResponseOptions()...) }) + mux.Handle(http.MethodPost, pattern_AuthService_RefreshToken_1, func(w http.ResponseWriter, req *http.Request, pathParams map[string]string) { + ctx, cancel := context.WithCancel(req.Context()) + defer cancel() + var stream runtime.ServerTransportStream + ctx = grpc.NewContextWithServerTransportStream(ctx, &stream) + inboundMarshaler, outboundMarshaler := runtime.MarshalerForRequest(mux, req) + annotatedContext, err := runtime.AnnotateIncomingContext(ctx, mux, req, "/auth.v1.AuthService/RefreshToken", runtime.WithHTTPPathPattern("/_pd/api/v2/auth/refresh")) + if err != nil { + runtime.HTTPError(ctx, mux, outboundMarshaler, w, req, err) + return + } + resp, md, err := local_request_AuthService_RefreshToken_1(annotatedContext, inboundMarshaler, server, req, pathParams) + md.HeaderMD, md.TrailerMD = metadata.Join(md.HeaderMD, stream.Header()), metadata.Join(md.TrailerMD, stream.Trailer()) + annotatedContext = runtime.NewServerMetadataContext(annotatedContext, md) + if err != nil { + runtime.HTTPError(annotatedContext, mux, outboundMarshaler, w, req, err) + return + } + forward_AuthService_RefreshToken_1(annotatedContext, mux, outboundMarshaler, w, req, resp, mux.GetForwardResponseOptions()...) + }) mux.Handle(http.MethodPost, pattern_AuthService_Logout_0, func(w http.ResponseWriter, req *http.Request, pathParams map[string]string) { ctx, cancel := context.WithCancel(req.Context()) defer cancel() @@ -320,6 +367,23 @@ func RegisterAuthServiceHandlerClient(ctx context.Context, mux *runtime.ServeMux } forward_AuthService_RefreshToken_0(annotatedContext, mux, outboundMarshaler, w, req, resp, mux.GetForwardResponseOptions()...) }) + mux.Handle(http.MethodPost, pattern_AuthService_RefreshToken_1, func(w http.ResponseWriter, req *http.Request, pathParams map[string]string) { + ctx, cancel := context.WithCancel(req.Context()) + defer cancel() + inboundMarshaler, outboundMarshaler := runtime.MarshalerForRequest(mux, req) + annotatedContext, err := runtime.AnnotateContext(ctx, mux, req, "/auth.v1.AuthService/RefreshToken", runtime.WithHTTPPathPattern("/_pd/api/v2/auth/refresh")) + if err != nil { + runtime.HTTPError(ctx, mux, outboundMarshaler, w, req, err) + return + } + resp, md, err := request_AuthService_RefreshToken_1(annotatedContext, inboundMarshaler, client, req, pathParams) + annotatedContext = runtime.NewServerMetadataContext(annotatedContext, md) + if err != nil { + runtime.HTTPError(annotatedContext, mux, outboundMarshaler, w, req, err) + return + } + forward_AuthService_RefreshToken_1(annotatedContext, mux, outboundMarshaler, w, req, resp, mux.GetForwardResponseOptions()...) + }) mux.Handle(http.MethodPost, pattern_AuthService_Logout_0, func(w http.ResponseWriter, req *http.Request, pathParams map[string]string) { ctx, cancel := context.WithCancel(req.Context()) defer cancel() @@ -344,6 +408,7 @@ var ( pattern_AuthService_LoginByGoogle_0 = runtime.MustPattern(runtime.NewPattern(1, []int{2, 0, 2, 1, 2, 2, 2, 3, 2, 4, 2, 5}, []string{"_pd", "api", "v1", "auth", "login", "google"}, "")) pattern_AuthService_LoginByOverleaf_0 = runtime.MustPattern(runtime.NewPattern(1, []int{2, 0, 2, 1, 2, 2, 2, 3, 2, 4, 2, 5}, []string{"_pd", "api", "v1", "auth", "login", "overleaf"}, "")) pattern_AuthService_RefreshToken_0 = runtime.MustPattern(runtime.NewPattern(1, []int{2, 0, 2, 1, 2, 2, 2, 3, 2, 4}, []string{"_pd", "api", "v1", "auth", "refresh"}, "")) + pattern_AuthService_RefreshToken_1 = runtime.MustPattern(runtime.NewPattern(1, []int{2, 0, 2, 1, 2, 2, 2, 3, 2, 4}, []string{"_pd", "api", "v2", "auth", "refresh"}, "")) pattern_AuthService_Logout_0 = runtime.MustPattern(runtime.NewPattern(1, []int{2, 0, 2, 1, 2, 2, 2, 3, 2, 4}, []string{"_pd", "api", "v1", "auth", "logout"}, "")) ) @@ -351,5 +416,6 @@ var ( forward_AuthService_LoginByGoogle_0 = runtime.ForwardResponseMessage forward_AuthService_LoginByOverleaf_0 = runtime.ForwardResponseMessage forward_AuthService_RefreshToken_0 = runtime.ForwardResponseMessage + forward_AuthService_RefreshToken_1 = runtime.ForwardResponseMessage forward_AuthService_Logout_0 = runtime.ForwardResponseMessage ) diff --git a/proto/auth/v1/auth.proto b/proto/auth/v1/auth.proto index 2078bbb3..fc50e975 100644 --- a/proto/auth/v1/auth.proto +++ b/proto/auth/v1/auth.proto @@ -23,6 +23,10 @@ service AuthService { option (google.api.http) = { post: "/_pd/api/v1/auth/refresh" body: "*" + additional_bindings { + post: "/_pd/api/v2/auth/refresh" + body: "*" + } }; } rpc Logout(LogoutRequest) returns (LogoutResponse) { diff --git a/webapp/_webapp/src/libs/apiclient.ts b/webapp/_webapp/src/libs/apiclient.ts index 0552f4ca..fd5163d2 100644 --- a/webapp/_webapp/src/libs/apiclient.ts +++ b/webapp/_webapp/src/libs/apiclient.ts @@ -120,8 +120,9 @@ class ApiClient { const errorData = error.response?.data; const errorPayload = fromJson(ErrorSchema, errorData); if (!options?.ignoreErrorToast) { - const message = errorPayload.message.replace(/^rpc error: code = Code\(\d+\) desc = /, ""); - errorToast(message + ` (${config.url})`, `Request Failed: ${ErrorCode[errorPayload.code]}`); + const message = this.cleanErrorMessage(errorPayload.message); + const title = this.getErrorTitle(errorPayload.code); + errorToast(message, title); } throw errorPayload; } @@ -129,6 +130,29 @@ class ApiClient { } } + private cleanErrorMessage(msg: string): string { + // Remove technical gRPC prefixes + return msg + .replace(/^rpc error: code = \w+ desc = /, "") + .replace(/^rpc error: code = Code\(\d+\) desc = /, ""); + } + + private getErrorTitle(code: ErrorCode): string { + const titles: Partial> = { + [ErrorCode.INVALID_TOKEN]: "Authentication Required", + [ErrorCode.INVALID_ACTOR]: "Session Invalid", + [ErrorCode.INVALID_USER]: "User Not Found", + [ErrorCode.PERMISSION_DENIED]: "Access Denied", + [ErrorCode.RECORD_NOT_FOUND]: "Not Found", + [ErrorCode.BAD_REQUEST]: "Invalid Request", + [ErrorCode.INTERNAL]: "Server Error", + [ErrorCode.INVALID_CREDENTIAL]: "Invalid Credentials", + [ErrorCode.INVALID_LLM_RESPONSE]: "AI Response Error", + [ErrorCode.PROJECT_OUT_OF_DATE]: "Project Outdated", + }; + return titles[code] || "Request Failed"; + } + async get(url: string, params?: object, options?: RequestOptions): Promise { return this.requestWithErrorToast( { diff --git a/webapp/_webapp/src/pkg/gen/apiclient/auth/v1/auth_pb.ts b/webapp/_webapp/src/pkg/gen/apiclient/auth/v1/auth_pb.ts index 1c0c4dc1..c8e55304 100644 --- a/webapp/_webapp/src/pkg/gen/apiclient/auth/v1/auth_pb.ts +++ b/webapp/_webapp/src/pkg/gen/apiclient/auth/v1/auth_pb.ts @@ -11,7 +11,7 @@ import type { Message } from "@bufbuild/protobuf"; * Describes the file auth/v1/auth.proto. */ export const file_auth_v1_auth: GenFile = /*@__PURE__*/ - fileDesc("ChJhdXRoL3YxL2F1dGgucHJvdG8SB2F1dGgudjEiLAoUTG9naW5CeUdvb2dsZVJlcXVlc3QSFAoMZ29vZ2xlX3Rva2VuGAEgASgJIj0KFUxvZ2luQnlHb29nbGVSZXNwb25zZRINCgV0b2tlbhgBIAEoCRIVCg1yZWZyZXNoX3Rva2VuGAIgASgJIjAKFkxvZ2luQnlPdmVybGVhZlJlcXVlc3QSFgoOb3ZlcmxlYWZfdG9rZW4YASABKAkiPwoXTG9naW5CeU92ZXJsZWFmUmVzcG9uc2USDQoFdG9rZW4YASABKAkSFQoNcmVmcmVzaF90b2tlbhgCIAEoCSIsChNSZWZyZXNoVG9rZW5SZXF1ZXN0EhUKDXJlZnJlc2hfdG9rZW4YASABKAkiPAoUUmVmcmVzaFRva2VuUmVzcG9uc2USDQoFdG9rZW4YASABKAkSFQoNcmVmcmVzaF90b2tlbhgCIAEoCSImCg1Mb2dvdXRSZXF1ZXN0EhUKDXJlZnJlc2hfdG9rZW4YASABKAkiEAoOTG9nb3V0UmVzcG9uc2Uy2wMKC0F1dGhTZXJ2aWNlEngKDUxvZ2luQnlHb29nbGUSHS5hdXRoLnYxLkxvZ2luQnlHb29nbGVSZXF1ZXN0Gh4uYXV0aC52MS5Mb2dpbkJ5R29vZ2xlUmVzcG9uc2UiKILT5JMCIjoBKiIdL19wZC9hcGkvdjEvYXV0aC9sb2dpbi9nb29nbGUSgAEKD0xvZ2luQnlPdmVybGVhZhIfLmF1dGgudjEuTG9naW5CeU92ZXJsZWFmUmVxdWVzdBogLmF1dGgudjEuTG9naW5CeU92ZXJsZWFmUmVzcG9uc2UiKoLT5JMCJDoBKiIfL19wZC9hcGkvdjEvYXV0aC9sb2dpbi9vdmVybGVhZhJwCgxSZWZyZXNoVG9rZW4SHC5hdXRoLnYxLlJlZnJlc2hUb2tlblJlcXVlc3QaHS5hdXRoLnYxLlJlZnJlc2hUb2tlblJlc3BvbnNlIiOC0+STAh06ASoiGC9fcGQvYXBpL3YxL2F1dGgvcmVmcmVzaBJdCgZMb2dvdXQSFi5hdXRoLnYxLkxvZ291dFJlcXVlc3QaFy5hdXRoLnYxLkxvZ291dFJlc3BvbnNlIiKC0+STAhw6ASoiFy9fcGQvYXBpL3YxL2F1dGgvbG9nb3V0Qn8KC2NvbS5hdXRoLnYxQglBdXRoUHJvdG9QAVoocGFwZXJkZWJ1Z2dlci9wa2cvZ2VuL2FwaS9hdXRoL3YxO2F1dGh2MaICA0FYWKoCB0F1dGguVjHKAgdBdXRoXFYx4gITQXV0aFxWMVxHUEJNZXRhZGF0YeoCCEF1dGg6OlYxYgZwcm90bzM", [file_google_api_annotations]); + fileDesc("ChJhdXRoL3YxL2F1dGgucHJvdG8SB2F1dGgudjEiLAoUTG9naW5CeUdvb2dsZVJlcXVlc3QSFAoMZ29vZ2xlX3Rva2VuGAEgASgJIj0KFUxvZ2luQnlHb29nbGVSZXNwb25zZRINCgV0b2tlbhgBIAEoCRIVCg1yZWZyZXNoX3Rva2VuGAIgASgJIjAKFkxvZ2luQnlPdmVybGVhZlJlcXVlc3QSFgoOb3ZlcmxlYWZfdG9rZW4YASABKAkiPwoXTG9naW5CeU92ZXJsZWFmUmVzcG9uc2USDQoFdG9rZW4YASABKAkSFQoNcmVmcmVzaF90b2tlbhgCIAEoCSIsChNSZWZyZXNoVG9rZW5SZXF1ZXN0EhUKDXJlZnJlc2hfdG9rZW4YASABKAkiPAoUUmVmcmVzaFRva2VuUmVzcG9uc2USDQoFdG9rZW4YASABKAkSFQoNcmVmcmVzaF90b2tlbhgCIAEoCSImCg1Mb2dvdXRSZXF1ZXN0EhUKDXJlZnJlc2hfdG9rZW4YASABKAkiEAoOTG9nb3V0UmVzcG9uc2Uy+wMKC0F1dGhTZXJ2aWNlEngKDUxvZ2luQnlHb29nbGUSHS5hdXRoLnYxLkxvZ2luQnlHb29nbGVSZXF1ZXN0Gh4uYXV0aC52MS5Mb2dpbkJ5R29vZ2xlUmVzcG9uc2UiKILT5JMCIjoBKiIdL19wZC9hcGkvdjEvYXV0aC9sb2dpbi9nb29nbGUSgAEKD0xvZ2luQnlPdmVybGVhZhIfLmF1dGgudjEuTG9naW5CeU92ZXJsZWFmUmVxdWVzdBogLmF1dGgudjEuTG9naW5CeU92ZXJsZWFmUmVzcG9uc2UiKoLT5JMCJDoBKiIfL19wZC9hcGkvdjEvYXV0aC9sb2dpbi9vdmVybGVhZhKPAQoMUmVmcmVzaFRva2VuEhwuYXV0aC52MS5SZWZyZXNoVG9rZW5SZXF1ZXN0Gh0uYXV0aC52MS5SZWZyZXNoVG9rZW5SZXNwb25zZSJCgtPkkwI8OgEqWh06ASoiGC9fcGQvYXBpL3YyL2F1dGgvcmVmcmVzaCIYL19wZC9hcGkvdjEvYXV0aC9yZWZyZXNoEl0KBkxvZ291dBIWLmF1dGgudjEuTG9nb3V0UmVxdWVzdBoXLmF1dGgudjEuTG9nb3V0UmVzcG9uc2UiIoLT5JMCHDoBKiIXL19wZC9hcGkvdjEvYXV0aC9sb2dvdXRCfwoLY29tLmF1dGgudjFCCUF1dGhQcm90b1ABWihwYXBlcmRlYnVnZ2VyL3BrZy9nZW4vYXBpL2F1dGgvdjE7YXV0aHYxogIDQVhYqgIHQXV0aC5WMcoCB0F1dGhcVjHiAhNBdXRoXFYxXEdQQk1ldGFkYXRh6gIIQXV0aDo6VjFiBnByb3RvMw", [file_google_api_annotations]); /** * @generated from message auth.v1.LoginByGoogleRequest From 1f10864ec6708be461469b1e5e2170a19c9e2f18 Mon Sep 17 00:00:00 2001 From: Junyi Date: Tue, 6 Jan 2026 16:53:03 +0800 Subject: [PATCH 2/6] Update internal/libs/shared/error.go Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> --- internal/libs/shared/error.go | 5 ----- 1 file changed, 5 deletions(-) diff --git a/internal/libs/shared/error.go b/internal/libs/shared/error.go index 1f1bc418..32bfe925 100644 --- a/internal/libs/shared/error.go +++ b/internal/libs/shared/error.go @@ -75,11 +75,6 @@ func makeErrorFunc( detail := lo.FirstOrEmpty(details) var errorMessage string switch v := detail.(type) { - case nil: - // Use default message when no detail is provided - if defaultMsg, ok := errorCodeMessages[errorCode]; ok { - errorMessage = defaultMsg - } case error: errorMessage = v.Error() case interface{ String() string }: From 310f3be891f4b12b721ebba3c76ade9518c1094f Mon Sep 17 00:00:00 2001 From: Junyi Hou Date: Tue, 6 Jan 2026 16:55:00 +0800 Subject: [PATCH 3/6] feat: Implement unspecified error handling, refine error messages, and remove frontend refresh token endpoint. --- internal/api/actor.go | 4 ++-- internal/libs/shared/error.go | 5 +++-- webapp/_webapp/src/libs/apiclient.ts | 1 + webapp/_webapp/src/query/api.ts | 7 ------- 4 files changed, 6 insertions(+), 11 deletions(-) diff --git a/internal/api/actor.go b/internal/api/actor.go index de41125c..36c9b7bc 100644 --- a/internal/api/actor.go +++ b/internal/api/actor.go @@ -18,7 +18,7 @@ func parseUserActor(ctx context.Context, token string, userService *services.Use claims, err := jwt.VerifyJwtToken(token) if err != nil { - return nil, shared.ErrInvalidToken("Token verification failed") + return nil, shared.ErrInvalidToken(err.Error()) } if len(claims.Audience) == 0 || claims.Audience[0] != "paperdebugger/user" { @@ -32,7 +32,7 @@ func parseUserActor(ctx context.Context, token string, userService *services.Use _, err = userService.GetUserByID(ctx, actorID) if err != nil { - return nil, shared.ErrInvalidUser("User account not found") + return nil, shared.ErrInvalidUser(err.Error()) } return &accesscontrol.Actor{ID: actorID}, nil diff --git a/internal/libs/shared/error.go b/internal/libs/shared/error.go index 1f1bc418..5962e8a4 100644 --- a/internal/libs/shared/error.go +++ b/internal/libs/shared/error.go @@ -13,6 +13,7 @@ import ( // errorCodeMessages provides default user-friendly messages for each error code var errorCodeMessages = map[sharedv1.ErrorCode]string{ + sharedv1.ErrorCode_ERROR_CODE_UNSPECIFIED: "An unspecified error occurred", sharedv1.ErrorCode_ERROR_CODE_UNKNOWN: "An unknown error occurred", sharedv1.ErrorCode_ERROR_CODE_INTERNAL: "Internal server error", sharedv1.ErrorCode_ERROR_CODE_BAD_REQUEST: "Bad request", @@ -87,8 +88,8 @@ func makeErrorFunc( default: errorMessage = fmt.Sprintf("%v", v) } - // If still empty, use a generic message - if errorMessage == "" || errorMessage == "" { + // Handle edge case where fmt.Sprintf might produce "" + if errorMessage == "" { if defaultMsg, ok := errorCodeMessages[errorCode]; ok { errorMessage = defaultMsg } else { diff --git a/webapp/_webapp/src/libs/apiclient.ts b/webapp/_webapp/src/libs/apiclient.ts index fd5163d2..b833442b 100644 --- a/webapp/_webapp/src/libs/apiclient.ts +++ b/webapp/_webapp/src/libs/apiclient.ts @@ -139,6 +139,7 @@ class ApiClient { private getErrorTitle(code: ErrorCode): string { const titles: Partial> = { + [ErrorCode.UNSPECIFIED]: "Unknown Error", [ErrorCode.INVALID_TOKEN]: "Authentication Required", [ErrorCode.INVALID_ACTOR]: "Session Invalid", [ErrorCode.INVALID_USER]: "User Not Found", diff --git a/webapp/_webapp/src/query/api.ts b/webapp/_webapp/src/query/api.ts index 82760a65..c6ab619e 100644 --- a/webapp/_webapp/src/query/api.ts +++ b/webapp/_webapp/src/query/api.ts @@ -6,8 +6,6 @@ import { LoginByOverleafResponseSchema, LogoutRequest, LogoutResponseSchema, - RefreshTokenRequest, - RefreshTokenResponseSchema, } from "../pkg/gen/apiclient/auth/v1/auth_pb"; import { CreateConversationMessageStreamRequest, @@ -71,11 +69,6 @@ export const loginByGoogle = async (data: PlainMessage) => return fromJson(LoginByGoogleResponseSchema, response); }; -export const refreshToken = async (data: PlainMessage) => { - const response = await apiclient.post("/auth/refresh", data); - return fromJson(RefreshTokenResponseSchema, response); -}; - export const logout = async (data: PlainMessage) => { const response = await apiclient.post("/auth/logout", data, { ignoreErrorToast: true, From fd73bd014dfe6b9405c5f1295538b6b302981d3a Mon Sep 17 00:00:00 2001 From: Junyi Date: Tue, 6 Jan 2026 17:02:24 +0800 Subject: [PATCH 4/6] Update webapp/_webapp/src/libs/apiclient.ts Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> --- webapp/_webapp/src/libs/apiclient.ts | 15 +++++++++++---- 1 file changed, 11 insertions(+), 4 deletions(-) diff --git a/webapp/_webapp/src/libs/apiclient.ts b/webapp/_webapp/src/libs/apiclient.ts index b833442b..a7b315fa 100644 --- a/webapp/_webapp/src/libs/apiclient.ts +++ b/webapp/_webapp/src/libs/apiclient.ts @@ -131,10 +131,17 @@ class ApiClient { } private cleanErrorMessage(msg: string): string { - // Remove technical gRPC prefixes - return msg - .replace(/^rpc error: code = \w+ desc = /, "") - .replace(/^rpc error: code = Code\(\d+\) desc = /, ""); + // Remove technical gRPC prefixes, mirroring backend behavior: + // strip everything up to and including "desc = " when the message + // starts with "rpc error:" and contains "desc = ". + if (msg.startsWith("rpc error:")) { + const marker = "desc = "; + const idx = msg.indexOf(marker); + if (idx !== -1) { + return msg.slice(idx + marker.length); + } + } + return msg; } private getErrorTitle(code: ErrorCode): string { From 8b3a63cfae6dbb878d972dd58407f340abee096a Mon Sep 17 00:00:00 2001 From: Junyi Hou Date: Tue, 6 Jan 2026 17:04:05 +0800 Subject: [PATCH 5/6] refactor: refine error titles for unspecified and unknown codes and remove nil error message fallback. --- internal/libs/shared/error.go | 8 -------- webapp/_webapp/src/libs/apiclient.ts | 5 +++-- 2 files changed, 3 insertions(+), 10 deletions(-) diff --git a/internal/libs/shared/error.go b/internal/libs/shared/error.go index 4ae0e0a0..d4021a58 100644 --- a/internal/libs/shared/error.go +++ b/internal/libs/shared/error.go @@ -83,14 +83,6 @@ func makeErrorFunc( default: errorMessage = fmt.Sprintf("%v", v) } - // Handle edge case where fmt.Sprintf might produce "" - if errorMessage == "" { - if defaultMsg, ok := errorCodeMessages[errorCode]; ok { - errorMessage = defaultMsg - } else { - errorMessage = "An error occurred" - } - } return status.Error(codes.Code(errorCode), errorMessage) } } diff --git a/webapp/_webapp/src/libs/apiclient.ts b/webapp/_webapp/src/libs/apiclient.ts index b833442b..52c85263 100644 --- a/webapp/_webapp/src/libs/apiclient.ts +++ b/webapp/_webapp/src/libs/apiclient.ts @@ -138,8 +138,9 @@ class ApiClient { } private getErrorTitle(code: ErrorCode): string { - const titles: Partial> = { - [ErrorCode.UNSPECIFIED]: "Unknown Error", + const titles: Record = { + [ErrorCode.UNSPECIFIED]: "Unspecified Error", + [ErrorCode.UNKNOWN]: "Unknown Error", [ErrorCode.INVALID_TOKEN]: "Authentication Required", [ErrorCode.INVALID_ACTOR]: "Session Invalid", [ErrorCode.INVALID_USER]: "User Not Found", From fedfd2d71a0bc7f39a8e7f2cc3cd59b555b4f62c Mon Sep 17 00:00:00 2001 From: Junyi Hou Date: Wed, 7 Jan 2026 22:54:50 +0800 Subject: [PATCH 6/6] feat: Enhance error message consistency and descriptiveness by adding default backend error messages and updating frontend error titles. --- internal/api/actor.go | 4 ++-- internal/libs/shared/error.go | 7 +++++++ webapp/_webapp/src/libs/apiclient.ts | 25 +++++++++++++------------ 3 files changed, 22 insertions(+), 14 deletions(-) diff --git a/internal/api/actor.go b/internal/api/actor.go index 36c9b7bc..c73fbf43 100644 --- a/internal/api/actor.go +++ b/internal/api/actor.go @@ -3,12 +3,12 @@ package api import ( "context" + "go.mongodb.org/mongo-driver/v2/bson" + "paperdebugger/internal/accesscontrol" "paperdebugger/internal/libs/jwt" "paperdebugger/internal/libs/shared" "paperdebugger/internal/services" - - "go.mongodb.org/mongo-driver/v2/bson" ) func parseUserActor(ctx context.Context, token string, userService *services.UserService) (*accesscontrol.Actor, error) { diff --git a/internal/libs/shared/error.go b/internal/libs/shared/error.go index d4021a58..00a18999 100644 --- a/internal/libs/shared/error.go +++ b/internal/libs/shared/error.go @@ -76,6 +76,13 @@ func makeErrorFunc( detail := lo.FirstOrEmpty(details) var errorMessage string switch v := detail.(type) { + case nil: + // Use default message from errorCodeMessages when no details provided + if msg, ok := errorCodeMessages[errorCode]; ok { + errorMessage = msg + } else { + errorMessage = "An error occurred" + } case error: errorMessage = v.Error() case interface{ String() string }: diff --git a/webapp/_webapp/src/libs/apiclient.ts b/webapp/_webapp/src/libs/apiclient.ts index df961acb..b506cb70 100644 --- a/webapp/_webapp/src/libs/apiclient.ts +++ b/webapp/_webapp/src/libs/apiclient.ts @@ -144,20 +144,21 @@ class ApiClient { return msg; } + // Error titles aligned with backend errorCodeMessages (error.go) for consistency private getErrorTitle(code: ErrorCode): string { const titles: Record = { - [ErrorCode.UNSPECIFIED]: "Unspecified Error", - [ErrorCode.UNKNOWN]: "Unknown Error", - [ErrorCode.INVALID_TOKEN]: "Authentication Required", - [ErrorCode.INVALID_ACTOR]: "Session Invalid", - [ErrorCode.INVALID_USER]: "User Not Found", - [ErrorCode.PERMISSION_DENIED]: "Access Denied", - [ErrorCode.RECORD_NOT_FOUND]: "Not Found", - [ErrorCode.BAD_REQUEST]: "Invalid Request", - [ErrorCode.INTERNAL]: "Server Error", - [ErrorCode.INVALID_CREDENTIAL]: "Invalid Credentials", - [ErrorCode.INVALID_LLM_RESPONSE]: "AI Response Error", - [ErrorCode.PROJECT_OUT_OF_DATE]: "Project Outdated", + [ErrorCode.UNSPECIFIED]: "An unspecified error occurred", + [ErrorCode.UNKNOWN]: "An unknown error occurred", + [ErrorCode.INVALID_TOKEN]: "Invalid or missing authentication token", + [ErrorCode.INVALID_ACTOR]: "Invalid actor or session", + [ErrorCode.INVALID_USER]: "User not found or invalid", + [ErrorCode.PERMISSION_DENIED]: "Permission denied", + [ErrorCode.RECORD_NOT_FOUND]: "Record not found", + [ErrorCode.BAD_REQUEST]: "Bad request", + [ErrorCode.INTERNAL]: "Internal server error", + [ErrorCode.INVALID_CREDENTIAL]: "Invalid credentials", + [ErrorCode.INVALID_LLM_RESPONSE]: "Invalid LLM response", + [ErrorCode.PROJECT_OUT_OF_DATE]: "Project is out of date", }; return titles[code] || "Request Failed"; }