#9284·bruno

gRPC: binary (`-bin`) trailers are corrupted before they reach the Trailers tab and `bru.grpc.response.trailers`

Author: Orteke2464Created Sep 17, 2026Updated Sep 17, 2026
Labelsbug

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.

  1. Unzip bruno-bin-trailer-repro.zip, then in bruno-bin-trailer-repro/:
    npm install
    npm start
    
    The server listens on 127.0.0.1:50051 and prints the expected trailer value.
  2. Import the attached bin-trailer-repro.opencollection.yml (Import Collection). The server exposes reflection. Ping request already targets 127.0.0.1:50051 and the repro.Repro/Ping method.
  3. Run Ping. It answers INVALID_ARGUMENT on purpose, with the trailer grpc-status-details-bin set to the bytes 08 03 1a d4 01 12 a0 01 f4 03.
  4. Look at the Trailers tab, or at the Bruno console: the after-call-end hook 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.