gRPC: binary (`-bin`) trailers are corrupted before they reach the Trailers tab and `bru.grpc.response.trailers`
I have checked the following:
- I have searched existing issues and found nothing related to my issue.
This bug is:
- Making Bruno unusable for me.
- Slowing me down but I'm able to continue working.
- Annoying.
- This feature was working in a previous version but is broken in the current release.
Bruno version
4.1.0-2026.09.16 (nightly)
Operating System
Windows
Specific OS Version / Distro
No response
Describe the bug
For -bin metadata keys (most notably grpc-status-details-bin, which carries google.rpc.Status rich error details) Bruno converts the value with value.toString(), i.e. decodes the raw protobuf bytes as UTF-8. Every byte sequence that is not valid UTF-8 becomes U+FFFD, so the value is unrecoverable: it shows up as garbage in the Trailers tab and arrives already corrupted in the new gRPC hooks (bru.grpc.response.trailers.get('grpc-status-details-bin') in after-call-end).
Per the gRPC spec, -bin metadata is binary and is transported as base64; the expected representation for display and scripting is the base64 string.
Root cause
packages/bruno-requests/src/grpc/grpc-client.js, processGrpcMetadata:
if (value && typeof value === 'object' && value.type === 'Buffer' && Array.isArray(value.data)) {
return { name, value: Buffer.from(value.data).toString('base64') };
}
return { name, value: value.toString() };
The base64 branch only matches a JSON-cloned Buffer ({ type: 'Buffer', data: [...] }). The handlers in setupGrpcEventHandlers call it with status.metadata.getMap() / error.metadata.getMap() directly, and @grpc/grpc-js Metadata.getMap() returns real Buffer instances for -bin keys (metadata.js: Buffer.isBuffer(v) ? Buffer.from(v) : v). A real Buffer has no type property, so it falls through to toString() (UTF-8). The same applies to the array branch.
Steps to reproduce
The attached
contains a minimal @grpc/grpc-js server that answers with a binary trailer, plus the proto.
- Unzip bruno-bin-trailer-repro.zip, then in
bruno-bin-trailer-repro/:
The server listens onnpm install npm start127.0.0.1:50051and prints the expected trailer value. - Import the attached
bin-trailer-repro.opencollection.yml(Import Collection). The server exposes reflection.Pingrequest already targets127.0.0.1:50051and therepro.Repro/Pingmethod. - Run
Ping. It answersINVALID_ARGUMENTon purpose, with the trailergrpc-status-details-binset to the bytes08 03 1a d4 01 12 a0 01 f4 03. - Look at the Trailers tab, or at the Bruno console: the
after-call-endhook in the collection prints the received value next to the expected one.
For comparison, the same call through grpcurl:
grpcurl -plaintext -v -d "{}" 127.0.0.1:50051 repro.Repro/Ping
Response trailers received:
grpc-status-details-bin: CAMa1AESoAH0Aw==
ERROR:
Code: InvalidArgument
Message: repro
Bruno shows the same trailer as "\b\u0003\u001a�\u0001\u0012�\u0001�\u0003".
Collection to reproduce
bin-trailer-repro.opencollection.yml
Screenshots/Live demo link
Look at "Trailers" tab
In Postman, it looks like a Base64-encoded string, just like when using grpcurl.
Source: usebruno/bruno