diff --git a/webapp/_webapp/src/components/message-entry-container/tools/paper-score-comment/add-comments-button.tsx b/webapp/_webapp/src/components/message-entry-container/tools/paper-score-comment/add-comments-button.tsx index 7311b41..472fa31 100644 --- a/webapp/_webapp/src/components/message-entry-container/tools/paper-score-comment/add-comments-button.tsx +++ b/webapp/_webapp/src/components/message-entry-container/tools/paper-score-comment/add-comments-button.tsx @@ -4,7 +4,7 @@ import { OverleafComment } from "../../../../pkg/gen/apiclient/project/v1/projec import { useSocketStore } from "../../../../stores/socket-store"; import { addClickedOverleafComment, hasClickedOverleafComment } from "../../../../libs/helpers"; import { acceptComments } from "../../../../query/api"; -import { fromJson } from "@bufbuild/protobuf"; +import { fromJson } from "../../../../libs/protobuf-utils"; import { CommentsAcceptedRequestSchema } from "../../../../pkg/gen/apiclient/comment/v1/comment_pb"; import { useConversationStore } from "../../../../stores/conversation/conversation-store"; diff --git a/webapp/_webapp/src/components/message-entry-container/tools/paper-score-comment/index.tsx b/webapp/_webapp/src/components/message-entry-container/tools/paper-score-comment/index.tsx index 473ad0f..1df2f86 100644 --- a/webapp/_webapp/src/components/message-entry-container/tools/paper-score-comment/index.tsx +++ b/webapp/_webapp/src/components/message-entry-container/tools/paper-score-comment/index.tsx @@ -1,4 +1,5 @@ -import { fromJson, JsonValue } from "@bufbuild/protobuf"; +import { JsonValue } from "@bufbuild/protobuf"; +import { fromJson } from "../../../../libs/protobuf-utils"; import { OverleafCommentSchema } from "../../../../pkg/gen/apiclient/project/v1/project_pb"; import { getProjectId } from "../../../../libs/helpers"; import { useEffect, useState } from "react"; diff --git a/webapp/_webapp/src/components/message-entry-container/tools/paper-score.tsx b/webapp/_webapp/src/components/message-entry-container/tools/paper-score.tsx index 0f113e5..34212b7 100644 --- a/webapp/_webapp/src/components/message-entry-container/tools/paper-score.tsx +++ b/webapp/_webapp/src/components/message-entry-container/tools/paper-score.tsx @@ -1,5 +1,5 @@ import { PaperScoreResultSchema } from "../../../pkg/gen/apiclient/project/v1/project_pb"; -import { fromJson } from "@bufbuild/protobuf"; +import { fromJson } from "../../../libs/protobuf-utils"; import { LoadingIndicator } from "../../loading-indicator"; import { logError } from "../../../libs/logger"; import { cn } from "@heroui/react"; diff --git a/webapp/_webapp/src/hooks/useSendMessageStream.ts b/webapp/_webapp/src/hooks/useSendMessageStream.ts index 0cbfcc9..fd43363 100644 --- a/webapp/_webapp/src/hooks/useSendMessageStream.ts +++ b/webapp/_webapp/src/hooks/useSendMessageStream.ts @@ -25,7 +25,7 @@ import { StreamPartEnd, } from "../pkg/gen/apiclient/chat/v2/chat_pb"; import { MessageEntry, MessageEntryStatus } from "../stores/conversation/types"; -import { fromJson } from "@bufbuild/protobuf"; +import { fromJson } from "../libs/protobuf-utils"; import { useConversationStore } from "../stores/conversation/conversation-store"; import { useListConversationsQuery } from "../query"; import { useSocketStore } from "../stores/socket-store"; diff --git a/webapp/_webapp/src/libs/apiclient.ts b/webapp/_webapp/src/libs/apiclient.ts index 0552f4c..05e305f 100644 --- a/webapp/_webapp/src/libs/apiclient.ts +++ b/webapp/_webapp/src/libs/apiclient.ts @@ -1,5 +1,6 @@ import axios, { AxiosError, AxiosInstance, AxiosRequestConfig } from "axios"; -import { fromJson, JsonValue } from "@bufbuild/protobuf"; +import { JsonValue } from "@bufbuild/protobuf"; +import { fromJson } from "./protobuf-utils"; import { RefreshTokenResponseSchema } from "../pkg/gen/apiclient/auth/v1/auth_pb"; import { GetUserResponseSchema } from "../pkg/gen/apiclient/user/v1/user_pb"; import { EventEmitter } from "events"; diff --git a/webapp/_webapp/src/libs/protobuf-utils.test.ts b/webapp/_webapp/src/libs/protobuf-utils.test.ts new file mode 100644 index 0000000..1e5558e --- /dev/null +++ b/webapp/_webapp/src/libs/protobuf-utils.test.ts @@ -0,0 +1,74 @@ +/** + * Test file to demonstrate that the protobuf-utils wrapper handles unknown fields gracefully. + * + * This test can be run manually to verify the fix. Since the project doesn't have + * a test runner configured, this serves as documentation of the expected behavior. + * + * To test manually: + * 1. Add a new field to a protobuf schema on the backend + * 2. Deploy the backend + * 3. Use an older version of the webapp (without regenerating protobuf files) + * 4. Verify that the webapp doesn't crash when receiving the new field + */ + +import { fromJson } from "./protobuf-utils"; +import { MessageSchema } from "../pkg/gen/apiclient/chat/v2/chat_pb"; + +/** + * Example: Testing that fromJson ignores unknown fields + * + * This would simulate a backend returning a message with a new field + * that doesn't exist in the current schema. + */ +function testIgnoreUnknownFields() { + // Simulate JSON response from backend with an extra field "newField" + const jsonWithUnknownField = { + messageId: "test-123", + payload: { + user: { + content: "Hello", + selectedText: "", + newFieldThatDoesntExistYet: "This is a new field from a newer backend version", + }, + }, + timestamp: "0", + }; + + try { + // This should NOT throw an error even though "newFieldThatDoesntExistYet" doesn't exist in the schema + const message = fromJson(MessageSchema, jsonWithUnknownField); + console.log("✓ Successfully parsed message with unknown field"); + console.log(" Message ID:", message.messageId); + console.log(" User content:", message.payload.user?.content); + return true; + } catch (error) { + console.error("✗ Failed to parse message with unknown field:", error); + return false; + } +} + +/** + * Example: Testing that fromJson still validates required fields + */ +function testRequiredFieldsStillValidated() { + // Missing required messageId field + const invalidJson = { + payload: { + user: { + content: "Hello", + }, + }, + }; + + try { + const message = fromJson(MessageSchema, invalidJson); + console.log("✓ Parsed message (messageId will be empty string):", message.messageId); + return true; + } catch (error) { + console.error("✗ Failed to parse message:", error); + return false; + } +} + +// Export test functions for manual testing +export { testIgnoreUnknownFields, testRequiredFieldsStillValidated }; diff --git a/webapp/_webapp/src/libs/protobuf-utils.ts b/webapp/_webapp/src/libs/protobuf-utils.ts new file mode 100644 index 0000000..f421af6 --- /dev/null +++ b/webapp/_webapp/src/libs/protobuf-utils.ts @@ -0,0 +1,17 @@ +import { DescMessage, fromJson as bufFromJson, JsonValue } from "@bufbuild/protobuf"; + +/** + * Wrapper around fromJson that ignores unknown fields to prevent crashes + * when new fields are added to the schema. + * + * This allows forward compatibility - older webapp versions can work with + * newer backend versions that introduce new fields. + */ +export function fromJson( + schema: Desc, + json: JsonValue, +): InstanceType { + return bufFromJson(schema, json, { + ignoreUnknownFields: true, + }); +} diff --git a/webapp/_webapp/src/query/api.ts b/webapp/_webapp/src/query/api.ts index 82760a6..437e8d4 100644 --- a/webapp/_webapp/src/query/api.ts +++ b/webapp/_webapp/src/query/api.ts @@ -57,7 +57,7 @@ import { GetUserInstructionsRequest, } from "../pkg/gen/apiclient/user/v1/user_pb"; import { PlainMessage } from "./types"; -import { fromJson } from "@bufbuild/protobuf"; +import { fromJson } from "../libs/protobuf-utils"; import { processStream } from "./utils"; import { CommentsAcceptedRequest, CommentsAcceptedResponseSchema } from "../pkg/gen/apiclient/comment/v1/comment_pb"; diff --git a/webapp/_webapp/src/query/utils.ts b/webapp/_webapp/src/query/utils.ts index 49780cf..8d77f67 100644 --- a/webapp/_webapp/src/query/utils.ts +++ b/webapp/_webapp/src/query/utils.ts @@ -1,4 +1,5 @@ -import { DescMessage, fromJson, JsonValue, JsonWriteOptions, toJson } from "@bufbuild/protobuf"; +import { DescMessage, JsonValue, JsonWriteOptions, toJson } from "@bufbuild/protobuf"; +import { fromJson } from "../libs/protobuf-utils"; import { logError } from "../libs/logger"; import { useDevtoolStore } from "../stores/devtool-store"; diff --git a/webapp/_webapp/src/stores/conversation/conversation-store.ts b/webapp/_webapp/src/stores/conversation/conversation-store.ts index 99d6df7..42c6d27 100644 --- a/webapp/_webapp/src/stores/conversation/conversation-store.ts +++ b/webapp/_webapp/src/stores/conversation/conversation-store.ts @@ -1,7 +1,7 @@ import { create } from "zustand"; import { persist, createJSONStorage } from "zustand/middleware"; import { Conversation, ConversationSchema } from "../../pkg/gen/apiclient/chat/v2/chat_pb"; -import { fromJson } from "@bufbuild/protobuf"; +import { fromJson } from "../../libs/protobuf-utils"; import { useConversationUiStore } from "./conversation-ui-store"; interface ConversationStore { diff --git a/webapp/_webapp/src/stores/conversation/handlers/converter.ts b/webapp/_webapp/src/stores/conversation/handlers/converter.ts index 1994c57..2c6bf2b 100644 --- a/webapp/_webapp/src/stores/conversation/handlers/converter.ts +++ b/webapp/_webapp/src/stores/conversation/handlers/converter.ts @@ -1,4 +1,4 @@ -import { fromJson } from "@bufbuild/protobuf"; +import { fromJson } from "../../../libs/protobuf-utils"; import { Conversation, Message, MessageSchema } from "../../../pkg/gen/apiclient/chat/v2/chat_pb"; import { MessageEntry, MessageEntryStatus } from "../types"; import { useStreamingMessageStore } from "../../streaming-message-store"; diff --git a/webapp/_webapp/src/stores/conversation/handlers/handleStreamError.ts b/webapp/_webapp/src/stores/conversation/handlers/handleStreamError.ts index 6bc4bc3..12981a6 100644 --- a/webapp/_webapp/src/stores/conversation/handlers/handleStreamError.ts +++ b/webapp/_webapp/src/stores/conversation/handlers/handleStreamError.ts @@ -5,7 +5,7 @@ import { getProjectId } from "../../../libs/helpers"; import { getCookies } from "../../../intermediate"; import { StreamingMessage } from "../../streaming-message-store"; import { MessageEntry, MessageEntryStatus } from "../types"; -import { fromJson } from "@bufbuild/protobuf"; +import { fromJson } from "../../../libs/protobuf-utils"; export async function handleStreamError( streamError: StreamError, diff --git a/webapp/_webapp/src/views/devtools/index.tsx b/webapp/_webapp/src/views/devtools/index.tsx index 75a6e59..f07f5f4 100644 --- a/webapp/_webapp/src/views/devtools/index.tsx +++ b/webapp/_webapp/src/views/devtools/index.tsx @@ -4,7 +4,7 @@ import { Button, Input } from "@heroui/react"; import { useStreamingMessageStore } from "../../stores/streaming-message-store"; import { MessageEntry, MessageEntryStatus } from "../../stores/conversation/types"; import { useConversationStore } from "../../stores/conversation/conversation-store"; -import { fromJson } from "@bufbuild/protobuf"; +import { fromJson } from "../../libs/protobuf-utils"; import { MessageSchema } from "../../pkg/gen/apiclient/chat/v2/chat_pb"; import { isEmptyConversation } from "../chat/helper"; import { useState } from "react"; diff --git a/webapp/_webapp/src/views/prompts/prompt-library-table.tsx b/webapp/_webapp/src/views/prompts/prompt-library-table.tsx index 576c99a..91308ef 100644 --- a/webapp/_webapp/src/views/prompts/prompt-library-table.tsx +++ b/webapp/_webapp/src/views/prompts/prompt-library-table.tsx @@ -2,7 +2,8 @@ import { cn, Spinner } from "@heroui/react"; import { useCallback, useState } from "react"; import { Prompt, PromptSchema } from "../../pkg/gen/apiclient/user/v1/user_pb"; import { ChatButton } from "../chat/header/chat-button"; -import { fromJson, toJson } from "@bufbuild/protobuf"; +import { toJson } from "@bufbuild/protobuf"; +import { fromJson } from "../../libs/protobuf-utils"; import { usePromptLibraryStore } from "../../stores/prompt-library-store"; type PromptLibraryTableProps = {