diff --git a/src/browser/history/protobuf/ProtobufBodyTrailer.test.ts b/src/browser/history/protobuf/ProtobufBodyTrailer.test.ts index 7989c1a673..9f41755773 100644 --- a/src/browser/history/protobuf/ProtobufBodyTrailer.test.ts +++ b/src/browser/history/protobuf/ProtobufBodyTrailer.test.ts @@ -73,6 +73,25 @@ it("retains allowed whitespace, visible text, and valid UTF-8 obs-text", async ( expect(result.examined_bytes).toBe(5 + Buffer.byteLength(text)); }); +it("excludes trailing whitespace from trailer field values", async () => { + const text = + "grpc-status: 0 \r\ngrpc-message: alpha\tbeta \t\r\nx-empty: \t\r\n"; + const result = await decodeProtobufBody( + trailerFrame(text), + "grpc-web", + source, + same, + new ProtobufBudget(), + ); + expect(result.status).toBe("complete"); + expect(result.frames[0]?.trailers).toEqual([ + { name: "grpc-status", value: "0" }, + { name: "grpc-message", value: "alpha\tbeta" }, + { name: "x-empty", value: "" }, + ]); + expect(result.examined_bytes).toBe(5 + Buffer.byteLength(text)); +}); + it.each(["grpc-web", "grpc-web-text"] as const)( "keeps valid trailer fields around an opaque non-UTF-8 value in %s framing", async (framing) => { diff --git a/src/browser/history/protobuf/ProtobufTrailers.ts b/src/browser/history/protobuf/ProtobufTrailers.ts index 1f382f0b27..08a651f63d 100644 --- a/src/browser/history/protobuf/ProtobufTrailers.ts +++ b/src/browser/history/protobuf/ProtobufTrailers.ts @@ -84,9 +84,13 @@ export const parseGrpcWebTrailers = ( "gRPC-web trailer field value contains a forbidden control character", }); } + // RFC 9110 section 5.5: a field value excludes surrounding whitespace. + let valueEnd = lineEnd; + while (valueEnd > valueStart && [9, 32].includes(bytes[valueEnd - 1]!)) + valueEnd -= 1; let value: string; try { - value = decoder.decode(bytes.subarray(valueStart, lineEnd)); + value = decoder.decode(bytes.subarray(valueStart, valueEnd)); } catch (cause) { if (!(cause instanceof TypeError)) throw cause; decoder = new TextDecoder("utf-8", { fatal: true, ignoreBOM: true });