Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -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";

Expand Down
Original file line number Diff line number Diff line change
@@ -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";
Expand Down
Original file line number Diff line number Diff line change
@@ -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";
Expand Down
2 changes: 1 addition & 1 deletion webapp/_webapp/src/hooks/useSendMessageStream.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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";
Expand Down
3 changes: 2 additions & 1 deletion webapp/_webapp/src/libs/apiclient.ts
Original file line number Diff line number Diff line change
@@ -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";
Expand Down
74 changes: 74 additions & 0 deletions webapp/_webapp/src/libs/protobuf-utils.test.ts
Original file line number Diff line number Diff line change
@@ -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 };
Comment on lines +1 to +74
Copy link

Copilot AI Jan 7, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The test file contains only exported functions but no executable tests. Since the project doesn't have a test runner configured (no test scripts in package.json, no jest/vitest/mocha dependencies), this file serves as documentation rather than automated tests. Consider either: 1) setting up a test runner and converting these to actual test cases, or 2) renaming the file to something like protobuf-utils.examples.ts or moving the documentation to a comment/doc file to better reflect its current purpose as manual testing documentation.

Copilot uses AI. Check for mistakes.
17 changes: 17 additions & 0 deletions webapp/_webapp/src/libs/protobuf-utils.ts
Original file line number Diff line number Diff line change
@@ -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<Desc extends DescMessage>(
schema: Desc,
json: JsonValue,
): InstanceType<Desc["message"]> {
return bufFromJson(schema, json, {
ignoreUnknownFields: true,
});
Comment on lines +10 to +16
Copy link

Copilot AI Jan 7, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The wrapper function doesn't support passing additional options that might be needed in the future. Consider adding an optional third parameter to allow callers to override or extend the default options while still preserving the ignoreUnknownFields: true default behavior. This would make the wrapper more flexible for future use cases where other JsonReadOptions might be needed (e.g., typeRegistry for custom types).

Copilot uses AI. Check for mistakes.
}
2 changes: 1 addition & 1 deletion webapp/_webapp/src/query/api.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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";

Expand Down
3 changes: 2 additions & 1 deletion webapp/_webapp/src/query/utils.ts
Original file line number Diff line number Diff line change
@@ -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";

Expand Down
Original file line number Diff line number Diff line change
@@ -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 {
Expand Down
Original file line number Diff line number Diff line change
@@ -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";
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down
2 changes: 1 addition & 1 deletion webapp/_webapp/src/views/devtools/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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";
Expand Down
3 changes: 2 additions & 1 deletion webapp/_webapp/src/views/prompts/prompt-library-table.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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 = {
Expand Down
Loading