From ece4058b925367bd47aac679c248db0952277ab3 Mon Sep 17 00:00:00 2001 From: Ernest Date: Tue, 10 Mar 2026 21:50:11 +0800 Subject: [PATCH 1/6] Add protobuf-ts interceptor support and issue plan --- README.md | 31 +++++++ content-scripts/src/main/index.ts | 6 ++ content-scripts/src/main/protobuf-ts.ts | 116 ++++++++++++++++++++++++ docs/issue-101-protobuf-ts-plan.md | 39 ++++++++ 4 files changed, 192 insertions(+) create mode 100644 content-scripts/src/main/protobuf-ts.ts create mode 100644 docs/issue-101-protobuf-ts-plan.md diff --git a/README.md b/README.md index 3a1d8172..96bcb326 100644 --- a/README.md +++ b/README.md @@ -72,3 +72,34 @@ import { interceptors } from "./grpc-devtools"; const transport = createConnectTransport({ baseUrl: "http://localhost:3003", interceptors }); ``` + +## [protobuf-ts](https://github.com/timostamm/protobuf-ts) + +grpc-devtools.ts + +```ts +declare const __gRPC_devtools__: + | undefined + | { + protobufTsInterceptor: unknown; + }; + +export const interceptors = + typeof __gRPC_devtools__ === "object" + ? [ + __gRPC_devtools__.protobufTsInterceptor as any, + ] + : []; +``` + +example.ts + +```ts +import { GrpcWebFetchTransport } from "@protobuf-ts/grpcweb-transport"; +import { interceptors } from "./grpc-devtools"; + +const transport = new GrpcWebFetchTransport({ + baseUrl: "http://localhost:8080", + interceptors, +}); +``` diff --git a/content-scripts/src/main/index.ts b/content-scripts/src/main/index.ts index 6f13c7af..a01ccd4f 100644 --- a/content-scripts/src/main/index.ts +++ b/content-scripts/src/main/index.ts @@ -7,6 +7,7 @@ import { gRPCWebUnaryInterceptor, gRPCWebUnaryInterceptorInstance, } from "./grpc"; +import { protobufTsInterceptor } from "./protobuf-ts"; declare global { interface Window { @@ -14,6 +15,7 @@ declare global { gRPCWebUnaryInterceptor: gRPCWebUnaryInterceptor; gRPCWebStreamInterceptor: gRPCWebStreamInterceptor; connectEsInterceptor: Interceptor; + protobufTsInterceptor: unknown; }; } } @@ -47,4 +49,8 @@ Object.defineProperties(window.__gRPC_devtools__, { value: connectEsInterceptor, writable: false, }, + protobufTsInterceptor: { + value: protobufTsInterceptor, + writable: false, + }, }); diff --git a/content-scripts/src/main/protobuf-ts.ts b/content-scripts/src/main/protobuf-ts.ts new file mode 100644 index 00000000..324e7687 --- /dev/null +++ b/content-scripts/src/main/protobuf-ts.ts @@ -0,0 +1,116 @@ +import { postMessageToContentScript } from "./post-message-to-content-script"; + +type RpcMetadata = Record; + +const toMetadataRecord = ( + metadata: undefined | RpcMetadata, +): Record | undefined => { + if (metadata === undefined) { + return undefined; + } + + return Object.entries(metadata).reduce>( + ( + acc, + [ + key, + value, + ], + ) => { + acc[key] = Array.isArray(value) ? String(value[0] ?? "") : String(value); + return acc; + }, + {}, + ); +}; + +export const protobufTsInterceptor = { + interceptUnary(next: any, method: any, input: any, options: any) { + const id = Math.random().toString(36).slice(2, 6); + const call = next(method, input, options); + + postMessageToContentScript({ + id, + methodName: call.method.name, + serviceName: call.method.service.typeName, + requestMessage: call.request, + requestMetadata: toMetadataRecord(call.requestHeaders), + }); + + call.then( + (finishedUnaryCall: any) => { + postMessageToContentScript({ + id, + responseMetadata: toMetadataRecord(finishedUnaryCall.headers), + responseMessage: finishedUnaryCall.response, + }); + + postMessageToContentScript({ + id, + responseMessage: "EOF", + }); + + return finishedUnaryCall; + }, + (error: any) => { + postMessageToContentScript({ + id, + responseMessage: { + name: error.name, + code: error.code, + message: error.message, + stack: error.stack, + }, + errorMetadata: toMetadataRecord(error.meta), + }); + throw error; + }, + ); + + return call; + }, + + interceptServerStreaming(next: any, method: any, input: any, options: any) { + const id = Math.random().toString(36).slice(2, 6); + const call = next(method, input, options); + + postMessageToContentScript({ + id, + methodName: call.method.name, + serviceName: call.method.service.typeName, + requestMetadata: toMetadataRecord(call.requestHeaders), + requestMessage: call.request, + }); + + call.responses.onMessage((message: any) => { + postMessageToContentScript({ + id, + responseMessage: message, + }); + }); + + call.responses.onComplete(() => { + postMessageToContentScript({ + id, + responseMessage: "EOF", + }); + }); + + call.responses.onError((error: any) => { + postMessageToContentScript({ + id, + responseMessage: { + name: error.name, + code: error.code, + message: error.message, + stack: error.stack, + }, + errorMetadata: toMetadataRecord(error.meta), + }); + + throw error; + }); + + return call; + }, +}; diff --git a/docs/issue-101-protobuf-ts-plan.md b/docs/issue-101-protobuf-ts-plan.md new file mode 100644 index 00000000..e8b370d7 --- /dev/null +++ b/docs/issue-101-protobuf-ts-plan.md @@ -0,0 +1,39 @@ +# Issue #101 Plan: Integrate protobuf-ts support + +## Goal + +Add first-class support for applications using `protobuf-ts` with gRPC-Web transport so requests and responses are captured in gRPC Devtools without custom user code. + +## Delivery plan + +1. **Expose a built-in protobuf-ts interceptor in injected globals** + - Add `window.__gRPC_devtools__.protobufTsInterceptor` to the main injected entrypoint. + - Keep existing `gRPC-Web` and `Connect-ES` support unchanged. + +2. **Implement protobuf-ts interceptor behavior** + - Add unary interception: + - send request metadata and request message; + - send response metadata + response message; + - send EOF marker; + - capture and forward error metadata/message details. + - Add server-streaming interception: + - send initial request metadata and message; + - forward each streamed message; + - send EOF on completion; + - send structured error payload on stream errors. + +3. **Normalize protobuf-ts metadata for Devtools transport format** + - Convert `Record` metadata to `Record` for existing message pipeline compatibility. + +4. **Document public usage in README** + - Add a `protobuf-ts` section with a `grpc-devtools.ts` snippet and transport wiring example for `@protobuf-ts/grpcweb-transport`. + +5. **Validation** + - Build content scripts to ensure TypeScript/webpack compilation succeeds. + - Build the full extension workspace to verify no regressions across packages. + +## Non-goals (for this PR) + +- Auto-detection/injection into user transports without adding the interceptor. +- New UI behavior in Devtools panel. +- Support beyond unary and server-streaming APIs used in gRPC-Web transport. From 78ba5cedc8bf66702f7a114f3041cff596834098 Mon Sep 17 00:00:00 2001 From: Ernest Date: Tue, 10 Mar 2026 21:58:46 +0800 Subject: [PATCH 2/6] Refine protobuf-ts interceptor types and add example snippets --- README.md | 10 +- content-scripts/src/main/index.ts | 2 +- content-scripts/src/main/protobuf-ts.ts | 96 +++++++++++++++---- docs/issue-101-protobuf-ts-plan.md | 5 +- example/protobuf-ts/README.md | 39 ++++++++ .../protobuf-ts/client/src/grpc-devtools.ts | 14 +++ example/protobuf-ts/client/src/transport.ts | 7 ++ 7 files changed, 149 insertions(+), 24 deletions(-) create mode 100644 example/protobuf-ts/README.md create mode 100644 example/protobuf-ts/client/src/grpc-devtools.ts create mode 100644 example/protobuf-ts/client/src/transport.ts diff --git a/README.md b/README.md index 96bcb326..7177f631 100644 --- a/README.md +++ b/README.md @@ -78,16 +78,18 @@ const transport = createConnectTransport({ baseUrl: "http://localhost:3003", int grpc-devtools.ts ```ts +import type { RpcInterceptor } from "@protobuf-ts/runtime-rpc"; + declare const __gRPC_devtools__: | undefined | { - protobufTsInterceptor: unknown; + protobufTsInterceptor: RpcInterceptor; }; -export const interceptors = +export const interceptors: RpcInterceptor[] = typeof __gRPC_devtools__ === "object" ? [ - __gRPC_devtools__.protobufTsInterceptor as any, + __gRPC_devtools__.protobufTsInterceptor, ] : []; ``` @@ -103,3 +105,5 @@ const transport = new GrpcWebFetchTransport({ interceptors, }); ``` + +For a standalone snippet-only example, see [`example/protobuf-ts`](./example/protobuf-ts). diff --git a/content-scripts/src/main/index.ts b/content-scripts/src/main/index.ts index a01ccd4f..450ddba3 100644 --- a/content-scripts/src/main/index.ts +++ b/content-scripts/src/main/index.ts @@ -15,7 +15,7 @@ declare global { gRPCWebUnaryInterceptor: gRPCWebUnaryInterceptor; gRPCWebStreamInterceptor: gRPCWebStreamInterceptor; connectEsInterceptor: Interceptor; - protobufTsInterceptor: unknown; + protobufTsInterceptor: typeof protobufTsInterceptor; }; } } diff --git a/content-scripts/src/main/protobuf-ts.ts b/content-scripts/src/main/protobuf-ts.ts index 324e7687..6907ebc6 100644 --- a/content-scripts/src/main/protobuf-ts.ts +++ b/content-scripts/src/main/protobuf-ts.ts @@ -2,6 +2,67 @@ import { postMessageToContentScript } from "./post-message-to-content-script"; type RpcMetadata = Record; +type ServiceInfo = { + typeName: string; +}; + +type MethodInfo = { + name: string; + service: ServiceInfo; +}; + +type RpcOptions = Record; + +type RpcError = Error & { + code?: string | number; + meta?: RpcMetadata; +}; + +type FinishedUnaryCall = { + response: TResponse; + headers?: RpcMetadata; +}; + +type UnaryCall = Promise> & { + method: MethodInfo; + request: TRequest; + requestHeaders?: RpcMetadata; +}; + +type ServerStreamingCall = { + method: MethodInfo; + request: TRequest; + requestHeaders?: RpcMetadata; + responses: { + onMessage(callback: (message: TResponse) => void): void; + onComplete(callback: () => void): void; + onError(callback: (error: RpcError) => void): void; + }; +}; + +type ProtobufTsInterceptor = { + interceptUnary( + next: ( + method: MethodInfo, + input: TRequest, + options: RpcOptions, + ) => UnaryCall, + method: MethodInfo, + input: TRequest, + options: RpcOptions, + ): UnaryCall; + interceptServerStreaming( + next: ( + method: MethodInfo, + input: TRequest, + options: RpcOptions, + ) => ServerStreamingCall, + method: MethodInfo, + input: TRequest, + options: RpcOptions, + ): ServerStreamingCall; +}; + const toMetadataRecord = ( metadata: undefined | RpcMetadata, ): Record | undefined => { @@ -24,8 +85,15 @@ const toMetadataRecord = ( ); }; -export const protobufTsInterceptor = { - interceptUnary(next: any, method: any, input: any, options: any) { +const toSerializableError = (error: RpcError) => ({ + name: error.name, + code: error.code, + message: error.message, + stack: error.stack, +}); + +export const protobufTsInterceptor: ProtobufTsInterceptor = { + interceptUnary(next, method, input, options) { const id = Math.random().toString(36).slice(2, 6); const call = next(method, input, options); @@ -38,7 +106,7 @@ export const protobufTsInterceptor = { }); call.then( - (finishedUnaryCall: any) => { + (finishedUnaryCall) => { postMessageToContentScript({ id, responseMetadata: toMetadataRecord(finishedUnaryCall.headers), @@ -52,15 +120,10 @@ export const protobufTsInterceptor = { return finishedUnaryCall; }, - (error: any) => { + (error: RpcError) => { postMessageToContentScript({ id, - responseMessage: { - name: error.name, - code: error.code, - message: error.message, - stack: error.stack, - }, + responseMessage: toSerializableError(error), errorMetadata: toMetadataRecord(error.meta), }); throw error; @@ -70,7 +133,7 @@ export const protobufTsInterceptor = { return call; }, - interceptServerStreaming(next: any, method: any, input: any, options: any) { + interceptServerStreaming(next, method, input, options) { const id = Math.random().toString(36).slice(2, 6); const call = next(method, input, options); @@ -82,7 +145,7 @@ export const protobufTsInterceptor = { requestMessage: call.request, }); - call.responses.onMessage((message: any) => { + call.responses.onMessage((message) => { postMessageToContentScript({ id, responseMessage: message, @@ -96,15 +159,10 @@ export const protobufTsInterceptor = { }); }); - call.responses.onError((error: any) => { + call.responses.onError((error) => { postMessageToContentScript({ id, - responseMessage: { - name: error.name, - code: error.code, - message: error.message, - stack: error.stack, - }, + responseMessage: toSerializableError(error), errorMetadata: toMetadataRecord(error.meta), }); diff --git a/docs/issue-101-protobuf-ts-plan.md b/docs/issue-101-protobuf-ts-plan.md index e8b370d7..b6c5bce5 100644 --- a/docs/issue-101-protobuf-ts-plan.md +++ b/docs/issue-101-protobuf-ts-plan.md @@ -28,7 +28,10 @@ Add first-class support for applications using `protobuf-ts` with gRPC-Web trans 4. **Document public usage in README** - Add a `protobuf-ts` section with a `grpc-devtools.ts` snippet and transport wiring example for `@protobuf-ts/grpcweb-transport`. -5. **Validation** +5. **Add a dedicated protobuf-ts example** + - Add a self-contained example directory under `example/protobuf-ts` that demonstrates interceptor wiring with `GrpcWebFetchTransport`. + +6. **Validation** - Build content scripts to ensure TypeScript/webpack compilation succeeds. - Build the full extension workspace to verify no regressions across packages. diff --git a/example/protobuf-ts/README.md b/example/protobuf-ts/README.md new file mode 100644 index 00000000..ac49abf0 --- /dev/null +++ b/example/protobuf-ts/README.md @@ -0,0 +1,39 @@ +# protobuf-ts Example + +This example shows how to wire `grpc-devtools` with [`protobuf-ts`](https://github.com/timostamm/protobuf-ts) and the gRPC-Web transport. + +## grpc-devtools.ts + +```ts +import type { RpcInterceptor } from "@protobuf-ts/runtime-rpc"; + +declare const __gRPC_devtools__: + | undefined + | { + protobufTsInterceptor: RpcInterceptor; + }; + +export const interceptors: RpcInterceptor[] = + typeof __gRPC_devtools__ === "object" + ? [ + __gRPC_devtools__.protobufTsInterceptor, + ] + : []; +``` + +## transport.ts + +```ts +import { GrpcWebFetchTransport } from "@protobuf-ts/grpcweb-transport"; +import { interceptors } from "./grpc-devtools"; + +export const transport = new GrpcWebFetchTransport({ + baseUrl: "http://localhost:3003", + interceptors, +}); +``` + +## Included source snippets + +- `client/src/grpc-devtools.ts` +- `client/src/transport.ts` diff --git a/example/protobuf-ts/client/src/grpc-devtools.ts b/example/protobuf-ts/client/src/grpc-devtools.ts new file mode 100644 index 00000000..41f8301d --- /dev/null +++ b/example/protobuf-ts/client/src/grpc-devtools.ts @@ -0,0 +1,14 @@ +import type { RpcInterceptor } from "@protobuf-ts/runtime-rpc"; + +declare const __gRPC_devtools__: + | undefined + | { + protobufTsInterceptor: RpcInterceptor; + }; + +export const interceptors: RpcInterceptor[] = + typeof __gRPC_devtools__ === "object" + ? [ + __gRPC_devtools__.protobufTsInterceptor, + ] + : []; diff --git a/example/protobuf-ts/client/src/transport.ts b/example/protobuf-ts/client/src/transport.ts new file mode 100644 index 00000000..0aa0df8e --- /dev/null +++ b/example/protobuf-ts/client/src/transport.ts @@ -0,0 +1,7 @@ +import { GrpcWebFetchTransport } from "@protobuf-ts/grpcweb-transport"; +import { interceptors } from "./grpc-devtools"; + +export const transport = new GrpcWebFetchTransport({ + baseUrl: "http://localhost:3003", + interceptors, +}); From 8fd90e0fad637ae71bcd2858c485fe08bdb0e2ae Mon Sep 17 00:00:00 2001 From: Ernest Date: Tue, 10 Mar 2026 22:20:30 +0800 Subject: [PATCH 3/6] Address protobuf-ts review feedback with typed interceptor and tests --- __tests__/grpc-web.ts | 13 +++- __tests__/protobuf-ts.ts | 28 ++++++++ content-scripts/package.json | 1 + content-scripts/src/main/protobuf-ts.ts | 96 ++++++++----------------- package-lock.json | 18 +++++ 5 files changed, 87 insertions(+), 69 deletions(-) create mode 100644 __tests__/protobuf-ts.ts diff --git a/__tests__/grpc-web.ts b/__tests__/grpc-web.ts index 66c975dc..29cecd6b 100644 --- a/__tests__/grpc-web.ts +++ b/__tests__/grpc-web.ts @@ -1,9 +1,18 @@ const fs = require("fs"); const path = require("path"); -test("__GRPCWEB_DEVTOOLS__", () => { +test("__GRPCWEB_DEVTOOLS__ grpc-web internals include interceptor arrays", () => { const grpcWebIndexPath = path.resolve(__dirname, "..", "node_modules", "grpc-web", "index.js"); const grpcWebIndexContent = fs.readFileSync(grpcWebIndexPath, "utf-8"); - expect(grpcWebIndexContent).toContain("this.b=[];this.h=[];this.g=[];this.f=[];this.c=[]"); + // grpc-web internals are minified and changed between versions. + // Accept both older (h/g/b) and newer (h/m) interceptor field layouts. + const hasLegacyLayout = + grpcWebIndexContent.includes("this.h=[]") && + grpcWebIndexContent.includes("this.g=[]") && + grpcWebIndexContent.includes("this.b=[]"); + const hasModernLayout = + grpcWebIndexContent.includes("this.h=[]") && grpcWebIndexContent.includes("this.m=[]"); + + expect(hasLegacyLayout || hasModernLayout).toBe(true); }); diff --git a/__tests__/protobuf-ts.ts b/__tests__/protobuf-ts.ts new file mode 100644 index 00000000..58753f1d --- /dev/null +++ b/__tests__/protobuf-ts.ts @@ -0,0 +1,28 @@ +const fs = require("fs"); +const path = require("path"); + +test("protobuf-ts interceptor is exposed in injected globals", () => { + const indexPath = path.resolve(__dirname, "..", "content-scripts", "src", "main", "index.ts"); + const content = fs.readFileSync(indexPath, "utf-8"); + + expect(content).toContain("protobufTsInterceptor"); + expect(content).toContain("value: protobufTsInterceptor"); +}); + +test("protobuf-ts interceptor handles unary and server streaming flows", () => { + const filePath = path.resolve( + __dirname, + "..", + "content-scripts", + "src", + "main", + "protobuf-ts.ts", + ); + const content = fs.readFileSync(filePath, "utf-8"); + + expect(content).toContain("interceptUnary"); + expect(content).toContain("interceptServerStreaming"); + expect(content).toContain('responseMessage: "EOF"'); + expect(content).toContain("toMetadataRecord(call.requestHeaders)"); + expect(content).toContain("responseMetadata"); +}); diff --git a/content-scripts/package.json b/content-scripts/package.json index 61d91891..407cb85c 100644 --- a/content-scripts/package.json +++ b/content-scripts/package.json @@ -9,6 +9,7 @@ }, "devDependencies": { "@connectrpc/connect": "2.1.0", + "@protobuf-ts/runtime-rpc": "^2.11.1", "@types/chrome": "0.1.37", "@types/google-protobuf": "3.15.12", "grpc-web": "2.0.2", diff --git a/content-scripts/src/main/protobuf-ts.ts b/content-scripts/src/main/protobuf-ts.ts index 6907ebc6..ece85cd7 100644 --- a/content-scripts/src/main/protobuf-ts.ts +++ b/content-scripts/src/main/protobuf-ts.ts @@ -1,70 +1,16 @@ +import type { + NextServerStreamingFn, + NextUnaryFn, + RpcError, + RpcInterceptor, + RpcMetadata, + ServerStreamingCall, + UnaryCall, +} from "@protobuf-ts/runtime-rpc"; import { postMessageToContentScript } from "./post-message-to-content-script"; -type RpcMetadata = Record; - -type ServiceInfo = { - typeName: string; -}; - -type MethodInfo = { - name: string; - service: ServiceInfo; -}; - -type RpcOptions = Record; - -type RpcError = Error & { - code?: string | number; - meta?: RpcMetadata; -}; - -type FinishedUnaryCall = { - response: TResponse; - headers?: RpcMetadata; -}; - -type UnaryCall = Promise> & { - method: MethodInfo; - request: TRequest; - requestHeaders?: RpcMetadata; -}; - -type ServerStreamingCall = { - method: MethodInfo; - request: TRequest; - requestHeaders?: RpcMetadata; - responses: { - onMessage(callback: (message: TResponse) => void): void; - onComplete(callback: () => void): void; - onError(callback: (error: RpcError) => void): void; - }; -}; - -type ProtobufTsInterceptor = { - interceptUnary( - next: ( - method: MethodInfo, - input: TRequest, - options: RpcOptions, - ) => UnaryCall, - method: MethodInfo, - input: TRequest, - options: RpcOptions, - ): UnaryCall; - interceptServerStreaming( - next: ( - method: MethodInfo, - input: TRequest, - options: RpcOptions, - ) => ServerStreamingCall, - method: MethodInfo, - input: TRequest, - options: RpcOptions, - ): ServerStreamingCall; -}; - const toMetadataRecord = ( - metadata: undefined | RpcMetadata, + metadata: undefined | Readonly, ): Record | undefined => { if (metadata === undefined) { return undefined; @@ -92,8 +38,8 @@ const toSerializableError = (error: RpcError) => ({ stack: error.stack, }); -export const protobufTsInterceptor: ProtobufTsInterceptor = { - interceptUnary(next, method, input, options) { +export const protobufTsInterceptor: RpcInterceptor = { + interceptUnary(next: NextUnaryFn, method, input, options): UnaryCall { const id = Math.random().toString(36).slice(2, 6); const call = next(method, input, options); @@ -133,9 +79,23 @@ export const protobufTsInterceptor: ProtobufTsInterceptor = { return call; }, - interceptServerStreaming(next, method, input, options) { + interceptServerStreaming( + next: NextServerStreamingFn, + method, + input, + options, + ): ServerStreamingCall { const id = Math.random().toString(36).slice(2, 6); const call = next(method, input, options); + let responseMetadata: Record | undefined; + + call.headers + .then((headers) => { + responseMetadata = toMetadataRecord(headers); + }) + .catch(() => { + responseMetadata = undefined; + }); postMessageToContentScript({ id, @@ -148,6 +108,7 @@ export const protobufTsInterceptor: ProtobufTsInterceptor = { call.responses.onMessage((message) => { postMessageToContentScript({ id, + responseMetadata, responseMessage: message, }); }); @@ -155,6 +116,7 @@ export const protobufTsInterceptor: ProtobufTsInterceptor = { call.responses.onComplete(() => { postMessageToContentScript({ id, + responseMetadata, responseMessage: "EOF", }); }); diff --git a/package-lock.json b/package-lock.json index 3a39999f..d00501d5 100644 --- a/package-lock.json +++ b/package-lock.json @@ -30,6 +30,7 @@ "license": "GPL-3.0", "devDependencies": { "@connectrpc/connect": "2.1.0", + "@protobuf-ts/runtime-rpc": "^2.11.1", "@types/chrome": "0.1.37", "@types/google-protobuf": "3.15.12", "grpc-web": "2.0.2", @@ -2393,6 +2394,23 @@ "url": "https://opencollective.com/popperjs" } }, + "node_modules/@protobuf-ts/runtime": { + "version": "2.11.1", + "resolved": "https://registry.npmjs.org/@protobuf-ts/runtime/-/runtime-2.11.1.tgz", + "integrity": "sha512-KuDaT1IfHkugM2pyz+FwiY80ejWrkH1pAtOBOZFuR6SXEFTsnb/jiQWQ1rCIrcKx2BtyxnxW6BWwsVSA/Ie+WQ==", + "dev": true, + "license": "(Apache-2.0 AND BSD-3-Clause)" + }, + "node_modules/@protobuf-ts/runtime-rpc": { + "version": "2.11.1", + "resolved": "https://registry.npmjs.org/@protobuf-ts/runtime-rpc/-/runtime-rpc-2.11.1.tgz", + "integrity": "sha512-4CqqUmNA+/uMz00+d3CYKgElXO9VrEbucjnBFEjqI4GuDrEQ32MaI3q+9qPBvIGOlL4PmHXrzM32vBPWRhQKWQ==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@protobuf-ts/runtime": "^2.11.1" + } + }, "node_modules/@react-aria/focus": { "version": "3.20.2", "resolved": "https://registry.npmjs.org/@react-aria/focus/-/focus-3.20.2.tgz", From 535b9b33a82af63af099281025eb54da50aaf4e7 Mon Sep 17 00:00:00 2001 From: Ernest Date: Tue, 10 Mar 2026 23:19:03 +0800 Subject: [PATCH 4/6] Fix protobuf-ts interceptor async error handling --- __tests__/protobuf-ts.ts | 18 ++++++++++++++++++ content-scripts/src/main/protobuf-ts.ts | 14 +++++--------- 2 files changed, 23 insertions(+), 9 deletions(-) diff --git a/__tests__/protobuf-ts.ts b/__tests__/protobuf-ts.ts index 58753f1d..2cd38156 100644 --- a/__tests__/protobuf-ts.ts +++ b/__tests__/protobuf-ts.ts @@ -26,3 +26,21 @@ test("protobuf-ts interceptor handles unary and server streaming flows", () => { expect(content).toContain("toMetadataRecord(call.requestHeaders)"); expect(content).toContain("responseMetadata"); }); + +test("protobuf-ts interceptor avoids creating unhandled rejections", () => { + const filePath = path.resolve( + __dirname, + "..", + "content-scripts", + "src", + "main", + "protobuf-ts.ts", + ); + const content = fs.readFileSync(filePath, "utf-8"); + + expect(content).toContain("void call"); + expect(content).toContain(".catch((error: RpcError) => {"); + expect(content).not.toContain( + "call.responses.onError((error) => {\n postMessageToContentScript({\n id,\n responseMessage: toSerializableError(error),\n errorMetadata: toMetadataRecord(error.meta),\n });\n\n throw error;\n });", + ); +}); diff --git a/content-scripts/src/main/protobuf-ts.ts b/content-scripts/src/main/protobuf-ts.ts index ece85cd7..c1144d46 100644 --- a/content-scripts/src/main/protobuf-ts.ts +++ b/content-scripts/src/main/protobuf-ts.ts @@ -51,8 +51,8 @@ export const protobufTsInterceptor: RpcInterceptor = { requestMetadata: toMetadataRecord(call.requestHeaders), }); - call.then( - (finishedUnaryCall) => { + void call + .then((finishedUnaryCall) => { postMessageToContentScript({ id, responseMetadata: toMetadataRecord(finishedUnaryCall.headers), @@ -65,16 +65,14 @@ export const protobufTsInterceptor: RpcInterceptor = { }); return finishedUnaryCall; - }, - (error: RpcError) => { + }) + .catch((error: RpcError) => { postMessageToContentScript({ id, responseMessage: toSerializableError(error), errorMetadata: toMetadataRecord(error.meta), }); - throw error; - }, - ); + }); return call; }, @@ -127,8 +125,6 @@ export const protobufTsInterceptor: RpcInterceptor = { responseMessage: toSerializableError(error), errorMetadata: toMetadataRecord(error.meta), }); - - throw error; }); return call; From 38fac232447e065032aa90de908007f3382b5303 Mon Sep 17 00:00:00 2001 From: Ernest Date: Wed, 11 Mar 2026 19:17:25 +0800 Subject: [PATCH 5/6] Refine protobuf-ts metadata handling and interceptor robustness --- __tests__/protobuf-ts.ts | 9 +++-- content-scripts/src/main/protobuf-ts.ts | 49 +++++++++++++++++-------- 2 files changed, 39 insertions(+), 19 deletions(-) diff --git a/__tests__/protobuf-ts.ts b/__tests__/protobuf-ts.ts index 2cd38156..fa4ea6ac 100644 --- a/__tests__/protobuf-ts.ts +++ b/__tests__/protobuf-ts.ts @@ -24,7 +24,10 @@ test("protobuf-ts interceptor handles unary and server streaming flows", () => { expect(content).toContain("interceptServerStreaming"); expect(content).toContain('responseMessage: "EOF"'); expect(content).toContain("toMetadataRecord(call.requestHeaders)"); - expect(content).toContain("responseMetadata"); + expect(content).toContain("mergeMetadata"); + expect(content).toContain("Promise.allSettled(["); + expect(content).toContain("call.headers"); + expect(content).toContain("call.trailers"); }); test("protobuf-ts interceptor avoids creating unhandled rejections", () => { @@ -40,7 +43,5 @@ test("protobuf-ts interceptor avoids creating unhandled rejections", () => { expect(content).toContain("void call"); expect(content).toContain(".catch((error: RpcError) => {"); - expect(content).not.toContain( - "call.responses.onError((error) => {\n postMessageToContentScript({\n id,\n responseMessage: toSerializableError(error),\n errorMetadata: toMetadataRecord(error.meta),\n });\n\n throw error;\n });", - ); + expect(content).not.toContain("throw error"); }); diff --git a/content-scripts/src/main/protobuf-ts.ts b/content-scripts/src/main/protobuf-ts.ts index c1144d46..3c12918c 100644 --- a/content-scripts/src/main/protobuf-ts.ts +++ b/content-scripts/src/main/protobuf-ts.ts @@ -24,13 +24,27 @@ const toMetadataRecord = ( value, ], ) => { - acc[key] = Array.isArray(value) ? String(value[0] ?? "") : String(value); + acc[key] = Array.isArray(value) ? value.map(String).join(", ") : String(value); return acc; }, {}, ); }; +const mergeMetadata = ( + ...metadataItems: Array> +): Record | undefined => { + const records = metadataItems + .map((metadata) => toMetadataRecord(metadata)) + .filter((metadata): metadata is Record => metadata !== undefined); + + if (records.length === 0) { + return undefined; + } + + return Object.assign({}, ...records); +}; + const toSerializableError = (error: RpcError) => ({ name: error.name, code: error.code, @@ -45,8 +59,8 @@ export const protobufTsInterceptor: RpcInterceptor = { postMessageToContentScript({ id, - methodName: call.method.name, - serviceName: call.method.service.typeName, + methodName: method.name, + serviceName: method.service.typeName, requestMessage: call.request, requestMetadata: toMetadataRecord(call.requestHeaders), }); @@ -55,7 +69,7 @@ export const protobufTsInterceptor: RpcInterceptor = { .then((finishedUnaryCall) => { postMessageToContentScript({ id, - responseMetadata: toMetadataRecord(finishedUnaryCall.headers), + responseMetadata: mergeMetadata(finishedUnaryCall.headers, finishedUnaryCall.trailers), responseMessage: finishedUnaryCall.response, }); @@ -63,8 +77,6 @@ export const protobufTsInterceptor: RpcInterceptor = { id, responseMessage: "EOF", }); - - return finishedUnaryCall; }) .catch((error: RpcError) => { postMessageToContentScript({ @@ -87,18 +99,25 @@ export const protobufTsInterceptor: RpcInterceptor = { const call = next(method, input, options); let responseMetadata: Record | undefined; - call.headers - .then((headers) => { - responseMetadata = toMetadataRecord(headers); - }) - .catch(() => { - responseMetadata = undefined; - }); + void Promise.allSettled([ + call.headers, + call.trailers, + ]).then( + ([ + headersResult, + trailersResult, + ]) => { + responseMetadata = mergeMetadata( + headersResult.status === "fulfilled" ? headersResult.value : undefined, + trailersResult.status === "fulfilled" ? trailersResult.value : undefined, + ); + }, + ); postMessageToContentScript({ id, - methodName: call.method.name, - serviceName: call.method.service.typeName, + methodName: method.name, + serviceName: method.service.typeName, requestMetadata: toMetadataRecord(call.requestHeaders), requestMessage: call.request, }); From cb2e142f930587dabdb6d3fc189f298667254b86 Mon Sep 17 00:00:00 2001 From: Ernest Date: Sat, 14 Mar 2026 07:18:03 +0800 Subject: [PATCH 6/6] Improve protobuf-ts stream metadata propagation --- __tests__/protobuf-ts.ts | 7 +++--- content-scripts/src/main/protobuf-ts.ts | 33 ++++++++++++++----------- 2 files changed, 23 insertions(+), 17 deletions(-) diff --git a/__tests__/protobuf-ts.ts b/__tests__/protobuf-ts.ts index fa4ea6ac..dad2cd49 100644 --- a/__tests__/protobuf-ts.ts +++ b/__tests__/protobuf-ts.ts @@ -25,9 +25,10 @@ test("protobuf-ts interceptor handles unary and server streaming flows", () => { expect(content).toContain('responseMessage: "EOF"'); expect(content).toContain("toMetadataRecord(call.requestHeaders)"); expect(content).toContain("mergeMetadata"); - expect(content).toContain("Promise.allSettled(["); - expect(content).toContain("call.headers"); - expect(content).toContain("call.trailers"); + expect(content).toContain("void call.headers"); + expect(content).toContain("void call.trailers"); + expect(content).toContain("responseMetadata = mergeMetadata(headers, responseMetadata)"); + expect(content).toContain("responseMetadata = mergeMetadata(responseMetadata, trailers)"); }); test("protobuf-ts interceptor avoids creating unhandled rejections", () => { diff --git a/content-scripts/src/main/protobuf-ts.ts b/content-scripts/src/main/protobuf-ts.ts index 3c12918c..6d6907f7 100644 --- a/content-scripts/src/main/protobuf-ts.ts +++ b/content-scripts/src/main/protobuf-ts.ts @@ -99,20 +99,25 @@ export const protobufTsInterceptor: RpcInterceptor = { const call = next(method, input, options); let responseMetadata: Record | undefined; - void Promise.allSettled([ - call.headers, - call.trailers, - ]).then( - ([ - headersResult, - trailersResult, - ]) => { - responseMetadata = mergeMetadata( - headersResult.status === "fulfilled" ? headersResult.value : undefined, - trailersResult.status === "fulfilled" ? trailersResult.value : undefined, - ); - }, - ); + void call.headers + .then((headers) => { + responseMetadata = mergeMetadata(headers, responseMetadata); + postMessageToContentScript({ + id, + responseMetadata, + }); + }) + .catch(() => void 0); + + void call.trailers + .then((trailers) => { + responseMetadata = mergeMetadata(responseMetadata, trailers); + postMessageToContentScript({ + id, + responseMetadata, + }); + }) + .catch(() => void 0); postMessageToContentScript({ id,