@@ -105,6 +105,13 @@ export class Http2CallStream extends Duplex implements CallStream {
105
105
// Status code mapped from :status. To be used if grpc-status is not received
106
106
private mappedStatusCode : Status = Status . UNKNOWN ;
107
107
108
+ // Promise objects that are re-assigned to resolving promises when headers
109
+ // or trailers received. Processing headers/trailers is asynchronous, so we
110
+ // can use these objects to await their completion. This helps us establish
111
+ // order of precedence when obtaining the status of the call.
112
+ private handlingHeaders = Promise . resolve ( ) ;
113
+ private handlingTrailers = Promise . resolve ( ) ;
114
+
108
115
// This is populated (non-null) if and only if the call has ended
109
116
private finalStatus : StatusObject | null = null ;
110
117
@@ -116,6 +123,11 @@ export class Http2CallStream extends Duplex implements CallStream {
116
123
this . filterStack = filterStackFactory . createFilter ( this ) ;
117
124
}
118
125
126
+ /**
127
+ * On first call, emits a 'status' event with the given StatusObject.
128
+ * Subsequent calls are no-ops.
129
+ * @param status The status of the call.
130
+ */
119
131
private endCall ( status : StatusObject ) : void {
120
132
if ( this . finalStatus === null ) {
121
133
this . finalStatus = status ;
@@ -135,12 +147,46 @@ export class Http2CallStream extends Duplex implements CallStream {
135
147
return canPush ;
136
148
}
137
149
150
+ private handleTrailers ( headers : http2 . IncomingHttpHeaders ) {
151
+ let code : Status = this . mappedStatusCode ;
152
+ let details = '' ;
153
+ let metadata : Metadata ;
154
+ try {
155
+ metadata = Metadata . fromHttp2Headers ( headers ) ;
156
+ } catch ( e ) {
157
+ metadata = new Metadata ( ) ;
158
+ }
159
+ let status : StatusObject = { code, details, metadata} ;
160
+ this . handlingTrailers = ( async ( ) => {
161
+ let finalStatus ;
162
+ try {
163
+ // Attempt to assign final status.
164
+ finalStatus = await this . filterStack . receiveTrailers ( Promise . resolve ( status ) ) ;
165
+ } catch ( error ) {
166
+ await this . handlingHeaders ;
167
+ // This is a no-op if the call was already ended when handling headers.
168
+ this . endCall ( {
169
+ code : Status . INTERNAL ,
170
+ details : 'Failed to process received status' ,
171
+ metadata : new Metadata ( )
172
+ } ) ;
173
+ return ;
174
+ }
175
+ // It's possible that headers were received but not fully handled yet.
176
+ // Give the headers handler an opportunity to end the call first,
177
+ // if an error occurred.
178
+ await this . handlingHeaders ;
179
+ // This is a no-op if the call was already ended when handling headers.
180
+ this . endCall ( finalStatus ) ;
181
+ } ) ( ) ;
182
+ }
183
+
138
184
attachHttp2Stream ( stream : http2 . ClientHttp2Stream ) : void {
139
185
if ( this . finalStatus !== null ) {
140
186
stream . rstWithCancel ( ) ;
141
187
} else {
142
188
this . http2Stream = stream ;
143
- stream . on ( 'response' , ( headers ) => {
189
+ stream . on ( 'response' , ( headers , flags ) => {
144
190
switch ( headers [ HTTP2_HEADER_STATUS ] ) {
145
191
// TODO(murgatroid99): handle 100 and 101
146
192
case '400' :
@@ -166,57 +212,27 @@ export class Http2CallStream extends Duplex implements CallStream {
166
212
}
167
213
delete headers [ HTTP2_HEADER_STATUS ] ;
168
214
delete headers [ HTTP2_HEADER_CONTENT_TYPE ] ;
169
- let metadata : Metadata ;
170
- try {
171
- metadata = Metadata . fromHttp2Headers ( headers ) ;
172
- } catch ( e ) {
173
- this . cancelWithStatus ( Status . UNKNOWN , e . message ) ;
174
- return ;
175
- }
176
- this . filterStack . receiveMetadata ( Promise . resolve ( metadata ) )
177
- . then (
178
- ( finalMetadata ) => {
179
- this . emit ( 'metadata' , finalMetadata ) ;
180
- } ,
181
- ( error ) => {
182
- this . cancelWithStatus ( Status . UNKNOWN , error . message ) ;
183
- } ) ;
184
- } ) ;
185
- stream . on ( 'trailers' , ( headers : http2 . IncomingHttpHeaders ) => {
186
- let code : Status = this . mappedStatusCode ;
187
- let details = '' ;
188
- if ( typeof headers [ 'grpc-status' ] === 'string' ) {
189
- let receivedCode = Number ( headers [ 'grpc-status' ] ) ;
190
- if ( receivedCode in Status ) {
191
- code = receivedCode ;
192
- } else {
193
- code = Status . UNKNOWN ;
215
+ if ( flags & http2 . constants . NGHTTP2_FLAG_END_STREAM ) {
216
+ this . handleTrailers ( headers ) ;
217
+ } else {
218
+ let metadata : Metadata ;
219
+ try {
220
+ metadata = Metadata . fromHttp2Headers ( headers ) ;
221
+ } catch ( error ) {
222
+ this . endCall ( { code : Status . UNKNOWN , details : error . message , metadata : new Metadata ( ) } ) ;
223
+ return ;
194
224
}
195
- delete headers [ 'grpc-status' ] ;
196
- }
197
- if ( typeof headers [ 'grpc-message' ] === 'string' ) {
198
- details = decodeURI ( headers [ 'grpc-message' ] as string ) ;
225
+ this . handlingHeaders =
226
+ this . filterStack . receiveMetadata ( Promise . resolve ( metadata ) )
227
+ . then ( ( finalMetadata ) => {
228
+ this . emit ( 'metadata' , finalMetadata ) ;
229
+ } ) . catch ( ( error ) => {
230
+ this . destroyHttp2Stream ( ) ;
231
+ this . endCall ( { code : Status . UNKNOWN , details : error . message , metadata : new Metadata ( ) } ) ;
232
+ } ) ;
199
233
}
200
- let metadata : Metadata ;
201
- try {
202
- metadata = Metadata . fromHttp2Headers ( headers ) ;
203
- } catch ( e ) {
204
- metadata = new Metadata ( ) ;
205
- }
206
- let status : StatusObject = { code, details, metadata} ;
207
- this . filterStack . receiveTrailers ( Promise . resolve ( status ) )
208
- . then (
209
- ( finalStatus ) => {
210
- this . endCall ( finalStatus ) ;
211
- } ,
212
- ( error ) => {
213
- this . endCall ( {
214
- code : Status . INTERNAL ,
215
- details : 'Failed to process received status' ,
216
- metadata : new Metadata ( )
217
- } ) ;
218
- } ) ;
219
234
} ) ;
235
+ stream . on ( 'trailers' , this . handleTrailers . bind ( this ) ) ;
220
236
stream . on ( 'data' , ( data ) => {
221
237
let readHead = 0 ;
222
238
let canPush = true ;
@@ -278,7 +294,7 @@ export class Http2CallStream extends Duplex implements CallStream {
278
294
this . unpushedReadMessages . push ( null ) ;
279
295
}
280
296
} ) ;
281
- stream . on ( 'streamClosed' , ( errorCode ) => {
297
+ stream . on ( 'close' , async ( errorCode ) => {
282
298
let code : Status ;
283
299
let details = '' ;
284
300
switch ( errorCode ) {
@@ -302,6 +318,13 @@ export class Http2CallStream extends Duplex implements CallStream {
302
318
default :
303
319
code = Status . INTERNAL ;
304
320
}
321
+ // This guarantees that if trailers were received, the value of the
322
+ // 'grpc-status' header takes precedence for emitted status data.
323
+ await this . handlingTrailers ;
324
+ // This is a no-op if trailers were received at all.
325
+ // This is OK, because status codes emitted here correspond to more
326
+ // catastrophic issues that prevent us from receiving trailers in the
327
+ // first place.
305
328
this . endCall ( { code : code , details : details , metadata : new Metadata ( ) } ) ;
306
329
} ) ;
307
330
stream . on ( 'error' , ( err : Error ) => {
@@ -326,8 +349,7 @@ export class Http2CallStream extends Duplex implements CallStream {
326
349
}
327
350
}
328
351
329
- cancelWithStatus ( status : Status , details : string ) : void {
330
- this . endCall ( { code : status , details : details , metadata : new Metadata ( ) } ) ;
352
+ private destroyHttp2Stream ( ) {
331
353
// The http2 stream could already have been destroyed if cancelWithStatus
332
354
// is called in response to an internal http2 error.
333
355
if ( this . http2Stream !== null && ! this . http2Stream . destroyed ) {
@@ -337,6 +359,16 @@ export class Http2CallStream extends Duplex implements CallStream {
337
359
}
338
360
}
339
361
362
+ cancelWithStatus ( status : Status , details : string ) : void {
363
+ this . destroyHttp2Stream ( ) ;
364
+ ( async ( ) => {
365
+ // If trailers are currently being processed, the call should be ended
366
+ // by handleTrailers instead.
367
+ await this . handlingTrailers ;
368
+ this . endCall ( { code : status , details : details , metadata : new Metadata ( ) } ) ;
369
+ } ) ( ) ;
370
+ }
371
+
340
372
getDeadline ( ) : Deadline {
341
373
return this . options . deadline ;
342
374
}
0 commit comments