1- import { status as Status } from "@grpc/grpc-js" ;
1+ import { Metadata , status as Status } from "@grpc/grpc-js" ;
22
33export interface GrpcErrorResponse {
44 status : Status ;
@@ -9,7 +9,9 @@ export interface GrpcSuccessResponse {
99 message : Uint8Array ;
1010}
1111
12- export type GrpcResponse = GrpcSuccessResponse | GrpcErrorResponse ;
12+ export type GrpcResponse = ( GrpcSuccessResponse | GrpcErrorResponse ) & {
13+ trailers ?: Metadata ;
14+ } ;
1315
1416function fourBytesLength ( sized : { length : number } ) : Uint8Array {
1517 const arr = new Uint8Array ( 4 ) ; // an Int32 takes 4 bytes
@@ -18,6 +20,77 @@ function fourBytesLength(sized: { length: number }): Uint8Array {
1820 return arr ;
1921}
2022
23+ export function decodeGrpcWebBody ( bodyBuffer : Buffer ) : GrpcResponse {
24+ if ( bodyBuffer . length === 0 ) {
25+ throw new Error ( "Body has zero length, cannot decode!" ) ;
26+ }
27+
28+ const bodyRaw = new Uint8Array ( bodyBuffer ) ;
29+
30+ // layout:
31+ // status code: 1 byte
32+ // message length 4 bytes (int32 big endian)
33+ // the message itself (len defined above)
34+ // trailer start byte: 0x80
35+ // trailers length (same format as above)
36+ // trailers: concatenated `key:value\r\n`
37+ let offset = 0 ;
38+
39+ const status : number | undefined = bodyRaw . at ( offset ) ;
40+ offset += 1 ;
41+
42+ if ( status === undefined || ! ( status in Status ) ) {
43+ throw new Error ( `Unrecognised status code [${ status } ]` ) ;
44+ }
45+
46+ const bodyLength = readInt32Length ( bodyRaw , offset ) ;
47+ offset += 4 ;
48+
49+ const message = new Uint8Array ( bodyRaw . subarray ( offset , offset + bodyLength ) ) ;
50+
51+ offset += bodyLength ;
52+
53+ const trailersHeader = 0x80 ;
54+
55+ if ( bodyRaw . at ( offset ++ ) !== trailersHeader ) {
56+ throw new Error ( "Expected trailers header 0x80" ) ;
57+ }
58+
59+ const trailersLength = readInt32Length ( bodyRaw , offset ) ;
60+
61+ offset += 4 ;
62+
63+ const trailersView = new DataView ( bodyRaw . buffer , offset , trailersLength ) ;
64+
65+ const trailersString = new TextDecoder ( ) . decode ( trailersView ) . trim ( ) ;
66+
67+ const trailers = new Metadata ( ) ;
68+
69+ trailersString . split ( "\r\n" ) . forEach ( ( trailer ) => {
70+ const [ key , value ] = trailer . split ( ":" , 2 ) ;
71+ trailers . set ( key , value ) ;
72+ } ) ;
73+
74+ if ( status !== Status . OK ) {
75+ return {
76+ status,
77+ trailers,
78+ detail : trailers . get ( "grpc-message" ) [ 0 ] as string | undefined ,
79+ } ;
80+ }
81+
82+ return {
83+ message,
84+ trailers,
85+ } ;
86+ }
87+
88+ function readInt32Length ( data : Uint8Array , offset : number = 0 ) : number {
89+ const view = new DataView ( data . buffer ) ;
90+
91+ return view . getInt32 ( offset , false ) ;
92+ }
93+
2194export class GrpcUnknownStatus extends Error {
2295 constructor ( unknownStatus : unknown ) {
2396 super ( `An unknown status was provided: ${ unknownStatus } ` ) ;
@@ -67,3 +140,12 @@ export function grpcResponseToBuffer(response: GrpcResponse): Buffer {
67140 trailerMessage ,
68141 ] ) ;
69142}
143+
144+ /**
145+ * Remove the header information from the request body. This is the reverse of the `frameRequest` function that
146+ * gRPC-Web applies to wrap the message body up for transport
147+ * @see https://github.com/improbable-eng/grpc-web/blob/53aaf4cdc0fede7103c1b06f0cfc560c003a5c41/client/grpc-web/src/util.ts#L3
148+ */
149+ export function unframeRequest ( requestBody : Uint8Array ) : Uint8Array {
150+ return new Uint8Array ( requestBody ) . slice ( 5 ) ;
151+ }
0 commit comments