SReverseby Simpa Labs

gRPC over HTTP/2

Reverse engineering gRPC in Android applications

A gRPC call names its service in the URL and puts the message in a length-prefixed frame. Once you can read those two things, the rest follows.

What gRPC sends

A gRPC call is an HTTP/2 POST to a path built from the service and method names: /package.Service/Method. The content type is application/grpc or a variant that names the message encoding, and the request carries a te: trailers header.

The body is a sequence of length-prefixed messages. Each message starts with a one-byte compression flag, then a four-byte big-endian length, then that many bytes of protobuf. A unary call has one message in the request and one in the response. The result of the call arrives in HTTP/2 trailers as grpc-status and grpc-message rather than in the response headers, which is why a call can return HTTP 200 and still have failed.

Compression is negotiated per call. The one-byte flag in each message records whether that payload uses it, and the encoding is named in a header, so a stream can mix compressed and uncompressed messages.

Why HTTP/2 captures look wrong

HTTP/2 multiplexes several streams over one connection and compresses header blocks with HPACK. A packet capture shows binary frames and header blocks that reference a dynamic table, so headers are not readable in place the way HTTP/1.1 headers are. Request and response bodies also arrive interleaved with flow-control frames.

This is a tooling problem rather than a protocol mystery. A capture that understands HTTP/2 reassembles the streams and decompresses the headers. The path, the content type and the trailers then become visible, and the path alone names the service and method you need to reconstruct.

Finding the service definition in the APK

gRPC Java and Kotlin builds generate a class per service with a name ending in Grpc, plus message classes from the proto compiler. The generated code contains most of what you need.

  • A full method name string such as /package.Service/Method.
  • Constants for the service and method names, often named SERVICE_NAME and METHOD_NAME.
  • Calls into io.grpc.MethodDescriptor, ClientCalls and bindService.
  • Message classes with the same field number constants that plain protobuf builds carry.

Kotlin coroutine stubs wrap the same generated descriptor, so the names are still present. If the app uses a generated stub library or a hand-written HTTP/2 client, the method names may exist only as string constants, which are still enough to rebuild a .proto for the messages.

Reflection is a server-side feature that exposes the service definitions at run time. Some deployments enable it, and a client that asks for the descriptor set gets the full schema without touching the APK. Test for it before investing in reconstruction, because a positive answer removes most of the schema work.

Reconstructing the messages

Recover the .proto for each message the way you would for any protobuf body: read the field numbers from the wire, match them to field constants in the DEX, and rebuild the schema. gRPC adds an outer layer that is independent of the schema, so a length prefix misread as part of the message produces a decoder error near the start of the body. Confirm the prefix first, then decode.

Error details are sometimes attached as a trailing metadata entry holding a serialized status message. Those bytes decode with the same protobuf rules, and they often contain a field or an argument name that tells you what the server disliked.

Metadata is the HTTP/2 header block. An authorization token, a tenant identifier or a custom signature arrives as a header, and a deadline becomes the grpc-timeout header with a unit suffix. Reproducing a call means reproducing both the headers and the framed body.

Streaming and status codes

Streaming methods change the shape of the body without changing the framing. A server-streaming call returns several length-prefixed messages. A client-streaming call sends several and then half-closes. A bidirectional call keeps both directions open and the messages interleave according to HTTP/2 flow control. Any client you write must read messages until the stream ends rather than assume a single response.

Status codes tell you which layer refused a call. A code for an unauthenticated or permission-denied condition points at the metadata. A code for an unimplemented method points at the path or the service name. A code for an invalid argument points at the message body. Reading the trailer before changing your framing saves a lot of guessing.

Reviewed 28 September 2026 · SReverse research desk

Start your full APK reconstruction

Send the full APK

Projects start at $120. Choose WhatsApp or email, then attach the APK in the app that opens. We reply within one hour with the next step and send the fixed quote after review.

Want us to contact you?