Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Decoding protobuf response #584

Open
essequie1 opened this issue Sep 24, 2023 · 8 comments
Open

Decoding protobuf response #584

essequie1 opened this issue Sep 24, 2023 · 8 comments

Comments

@essequie1
Copy link

I recently switched up from protobuf-js to this library, i really liked the change and it is working fine.
The thing is that there was a chrome extension i used to check the requests being made in every client of the application, and it doesn't work with the clients generated by protobuf-ts.

I thought about making an extension myself as i didn't believe it would be that hard, and i actually was able to get the response body from all the requests from the clients i generated in a chrome extension i made, when tried decoding them from base64 to string it gives a string with non ascii characters and numbers in between the real data of the response.

This is the one of the respones i got:

const test = "AAAAAH4KIwgCEgRUZXN0GhdodHRwczovL3d3dy5nb29nbGUuY29tLyACChwIBRIFU3BlY3MaDy9zcGVjaWZpY2F0aW9ucyABCjkIBxIDV0lQGi4vc3BlY2lmaWNhdGlvbnMvc2VhcmNoP3N0YXR1cz1Xb3JrK0luK1Byb2dyZXNzIAE=gAAAABBncnBjLXN0YXR1czogMA0K"

Buffer.from(test, "base64").toString()

When decoded gives me this:
'~ #����Test��https://www.google.com/ � �����Specs��/specifications � 9����WIP�./specifications/search?status=Work+In+Progress �'

This is what the "grcp web dev tools" gives me:

{
  grpc: {
    method: '...url/GetFavorites',
    request: {},
    response: {
      favoritesList: [
        {
          id: 2,
          label: 'Test',
          url: 'https://www.google.com/',
          favoriteType: 2,
        },
        {
          id: 5,
          label: 'Specs',
          url: '/specifications',
          favoriteType: 1,
        },
        {
          id: 7,
          label: 'WIP',
          url: '/specifications/search?status=Work+In+Progress',
          favoriteType: 1,
        },
      ],
    },
  },
};

I tried in many ways but i didn't found that much information about it in the internet, so i came here to ask if anyone knows how can i decode the information in the response to a JSON or JS Object or at least a simple string?

Thanks!

@timostamm
Copy link
Owner

gRPC-web uses a message framing. It's superficially documented here. It's just length delimited, with space for a couple of flags.

You can take a look at the transport implementation here if you want to roll your own:

export async function readGrpcWebResponseBody(stream: WebResponseBodyStream, contentType: string | undefined | null, onFrame: FrameHandler): Promise<void> {

@essequie1
Copy link
Author

essequie1 commented Sep 25, 2023

Is the first part of that function just waiting for a request and then creates an Uint8Array from it, right?
I'm guessing i can just convert the base64 string from a response into an Uint8Array and pass it to a function that just does this:

function readGrpcWebResponseBody(uintArr, onFrame) {
  let byteQueue = uintArr;
  while (byteQueue.length >= 5 && byteQueue[0] === GrpcWebFrame.DATA) {
    let msgLen = 0;

    for (let i = 1; i < 5; i++) {
      msgLen = (msgLen << 8) + byteQueue[i];
    }

    if (byteQueue.length - 5 >= msgLen) {
      // we have the entire message
      onFrame(GrpcWebFrame.DATA, byteQueue.subarray(5, 5 + msgLen));
      byteQueue = byteQueue.subarray(5 + msgLen);
    } else {
      break;
    }
  }
}

What i don't understand is how onFrame works, because it takes the "O" from the method of the response and uses .fromBinary() passing the actual response as a parameter?

Sorry for the misunderstanding but i don't really know that much about this and i can't actually wrap my mind around how this works.

PS.: How the "grpc-web-dev-tools" works is that it adds interceptors to all the clients and decodes or captures the responses from them. I don't really want to do that. What i'm doing right now is just capture the request responses when they finish and i don't really know if the information i got from the response is enought to actually decode the messages.

@timostamm
Copy link
Owner

gRPC-web uses a message framing. The function splits an incoming stream of binary data into individual frames.

i don't really know if the information i got from the response is enought to actually decode the messages.

To split the frames, yes. To parse protobuf messages from binary, no. See https://protobuf.dev/programming-guides/encoding/

@jcready
Copy link
Contributor

jcready commented Sep 26, 2023

You can get the "grpc-web-dev-tools" extension working with protobuf-ts by adding this interceptor to your grpc-web transport:

import type { RpcInterceptor } from '@protobuf-ts/runtime-rpc';
import { RpcError } from '@protobuf-ts/runtime-rpc';

const type = '__GRPCWEB_DEVTOOLS__';

export const grpcWebDevToolsInterceptor: RpcInterceptor = {
    interceptUnary(next, method, input, options) {
        const res = next(method, input, options);
        // @ts-ignore
        if (window.__GRPCWEB_DEVTOOLS__) {
            const methodType = 'unary';
            const m = `${method.service.typeName}/${method.name}`;
            const request = method.I.toJson(res.request);
            void res.then((value) => {
                window.postMessage({
                    type,
                    method: m,
                    methodType,
                    request,
                    response: method.O.toJson(value.response),
                });
            }).catch((e) => {
                if (e instanceof RpcError) {
                    window.postMessage({
                        type,
                        method: m,
                        methodType,
                        request,
                        error: {
                            code: e.code,
                            message: e.message,
                        },
                    });
                }
            });
        }
        return res;
    },
    interceptServerStreaming(next, method, input, options) {
        const res = next(method, input, options);
        // @ts-ignore
        if (window.__GRPCWEB_DEVTOOLS__) {
            const m = `${method.service.typeName}/${method.name}`;
            const methodType = 'server_streaming';
            window.postMessage({
                type,
                method: m,
                methodType,
                request: method.I.toJson(res.request),
            });
            res.responses.onMessage((value) => {
                window.postMessage({
                    type,
                    method: m,
                    methodType,
                    response: method.O.toJson(value),
                });
            });
            res.responses.onError((e) => {
                if (e instanceof RpcError) {
                    window.postMessage({
                        type,
                        method: m,
                        methodType,
                        error: {
                            code: e.code,
                            message: e.message,
                        },
                    });
                }
            });
            res.responses.onComplete(() => {
                window.postMessage({
                    type,
                    method: m,
                    methodType,
                    response: 'EOF',
                });
            });
        }
        return res;
    },
};

@essequie1
Copy link
Author

You can get the "grpc-web-dev-tools" extension working with protobuf-ts by adding this interceptor to your grpc-web transport:

Does this have an impact in performance? Thank you anyways!

@jcready
Copy link
Contributor

jcready commented Sep 26, 2023

Does this have an impact in performance?

Only if you have the "grpc-web-dev-tools" extension enabled.

@glmnet
Copy link

glmnet commented Oct 17, 2023

@jcready I'm using the code you posted and is failing for stream because m is no longer the method name on the onMessage callback

@jcready
Copy link
Contributor

jcready commented Oct 17, 2023

Ah, sorry about that. I believe I've fixed the code above.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

No branches or pull requests

4 participants