Skip to content

Commit f683dbb

Browse files
ktr0731johanbrandhorst
authored andcommitted
Fix gRPC Web spec violation related to header/trailer names (improbable-eng#271)
Ensures in-body trailer keys are lower case as per the spec.
1 parent 18d2e9a commit f683dbb

File tree

5 files changed

+107
-25
lines changed

5 files changed

+107
-25
lines changed

go/grpcweb/grpc_web_response.go

+2-2
Original file line numberDiff line numberDiff line change
@@ -114,9 +114,9 @@ func (w *grpcWebResponse) copyTrailersToPayload() {
114114
w.wrapped.(http.Flusher).Flush()
115115
}
116116

117-
func (w *grpcWebResponse) extractTrailerHeaders() http.Header {
117+
func (w *grpcWebResponse) extractTrailerHeaders() trailer {
118118
flushedHeaders := w.wrapped.Header()
119-
trailerHeaders := make(http.Header)
119+
trailerHeaders := trailer{make(http.Header)}
120120
for k, vv := range w.headers {
121121
// Skip the pre-annoucement of Trailer headers. Don't add them to the response headers.
122122
if strings.ToLower(k) == "trailer" {

go/grpcweb/trailer.go

+32
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,32 @@
1+
//Copyright 2018 Improbable. All Rights Reserved.
2+
// See LICENSE for licensing terms.
3+
4+
package grpcweb
5+
6+
import (
7+
"net/http"
8+
"strings"
9+
)
10+
11+
// gRPC-Web spec says that must use lower-case header/trailer names.
12+
// See "HTTP wire protocols" section in
13+
// https://github.com/grpc/grpc/blob/master/doc/PROTOCOL-WEB.md#protocol-differences-vs-grpc-over-http2
14+
type trailer struct {
15+
http.Header
16+
}
17+
18+
func (t trailer) Add(key, value string) {
19+
key = strings.ToLower(key)
20+
t.Header[key] = append(t.Header[key], value)
21+
}
22+
23+
func (t trailer) Get(key string) string {
24+
if t.Header == nil {
25+
return ""
26+
}
27+
v := t.Header[key]
28+
if len(v) == 0 {
29+
return ""
30+
}
31+
return v[0]
32+
}

go/grpcweb/trailer_test.go

+11
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,11 @@
1+
package grpcweb
2+
3+
import "net/http"
4+
5+
type Trailer struct {
6+
trailer
7+
}
8+
9+
func HTTPTrailerToGrpcWebTrailer(httpTrailer http.Header) Trailer {
10+
return Trailer{trailer{httpTrailer}}
11+
}

go/grpcweb/wrapper.go

+1-2
Original file line numberDiff line numberDiff line change
@@ -4,10 +4,9 @@
44
package grpcweb
55

66
import (
7+
"context"
78
"net/http"
89
"net/url"
9-
10-
"context"
1110
"strings"
1211
"time"
1312

go/grpcweb/wrapper_test.go

+61-21
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,10 @@ import (
2222
"time"
2323

2424
"github.com/golang/protobuf/proto"
25+
google_protobuf "github.com/golang/protobuf/ptypes/empty"
26+
"github.com/improbable-eng/grpc-web/go/grpcweb"
27+
testproto "github.com/improbable-eng/grpc-web/test/go/_proto/improbable/grpcweb/test"
28+
"github.com/mwitkow/go-conntrack/connhelpers"
2529
"github.com/stretchr/testify/assert"
2630
"github.com/stretchr/testify/require"
2731
"github.com/stretchr/testify/suite"
@@ -32,11 +36,6 @@ import (
3236
"google.golang.org/grpc/credentials"
3337
"google.golang.org/grpc/grpclog"
3438
"google.golang.org/grpc/metadata"
35-
36-
google_protobuf "github.com/golang/protobuf/ptypes/empty"
37-
"github.com/improbable-eng/grpc-web/go/grpcweb"
38-
testproto "github.com/improbable-eng/grpc-web/test/go/_proto/improbable/grpcweb/test"
39-
"github.com/mwitkow/go-conntrack/connhelpers"
4039
)
4140

4241
var (
@@ -112,7 +111,7 @@ func (s *GrpcWebWrapperTestSuite) makeRequest(verb string, method string, header
112111
return resp, err
113112
}
114113

115-
func (s *GrpcWebWrapperTestSuite) makeGrpcRequest(method string, reqHeaders http.Header, requestMessages [][]byte) (headers http.Header, trailers http.Header, responseMessages [][]byte, err error) {
114+
func (s *GrpcWebWrapperTestSuite) makeGrpcRequest(method string, reqHeaders http.Header, requestMessages [][]byte) (headers http.Header, trailers grpcweb.Trailer, responseMessages [][]byte, err error) {
116115
writer := new(bytes.Buffer)
117116
for _, msgBytes := range requestMessages {
118117
grpcPreamble := []byte{0, 0, 0, 0, 0}
@@ -122,33 +121,32 @@ func (s *GrpcWebWrapperTestSuite) makeGrpcRequest(method string, reqHeaders http
122121
}
123122
resp, err := s.makeRequest("POST", method, reqHeaders, writer)
124123
if err != nil {
125-
return nil, nil, nil, err
124+
return nil, grpcweb.Trailer{}, nil, err
126125
}
127126
defer resp.Body.Close()
128127
contents, err := ioutil.ReadAll(resp.Body)
129128
if err != nil {
130-
return nil, nil, nil, err
129+
return nil, grpcweb.Trailer{}, nil, err
131130
}
132131
reader := bytes.NewReader(contents)
133-
trailers = make(http.Header)
134132
for {
135133
grpcPreamble := []byte{0, 0, 0, 0, 0}
136134
readCount, err := reader.Read(grpcPreamble)
137135
if err == io.EOF {
138136
break
139137
}
140138
if readCount != 5 || err != nil {
141-
return nil, nil, nil, fmt.Errorf("Unexpected end of body in preamble: %v", err)
139+
return nil, grpcweb.Trailer{}, nil, fmt.Errorf("Unexpected end of body in preamble: %v", err)
142140
}
143141
payloadLength := binary.BigEndian.Uint32(grpcPreamble[1:])
144142
payloadBytes := make([]byte, payloadLength)
145143

146144
readCount, err = reader.Read(payloadBytes)
147145
if uint32(readCount) != payloadLength || err != nil {
148-
return nil, nil, nil, fmt.Errorf("Unexpected end of msg: %v", err)
146+
return nil, grpcweb.Trailer{}, nil, fmt.Errorf("Unexpected end of msg: %v", err)
149147
}
150148
if grpcPreamble[0]&(1<<7) == (1 << 7) { // MSB signifies the trailer parser
151-
trailers = readHeadersFromBytes(payloadBytes)
149+
trailers = readTrailersFromBytes(s.T(), payloadBytes)
152150
} else {
153151
responseMessages = append(responseMessages, payloadBytes)
154152
}
@@ -166,7 +164,7 @@ func (s *GrpcWebWrapperTestSuite) TestPingEmpty() {
166164
assert.Equal(s.T(), 1, len(responses), "PingEmpty is an unary response")
167165
s.assertTrailerGrpcCode(trailers, codes.OK, "")
168166
s.assertHeadersContainMetadata(headers, expectedHeaders)
169-
s.assertHeadersContainMetadata(trailers, expectedTrailers)
167+
s.assertTrailersContainMetadata(trailers, expectedTrailers)
170168
}
171169

172170
func (s *GrpcWebWrapperTestSuite) TestPing() {
@@ -179,7 +177,7 @@ func (s *GrpcWebWrapperTestSuite) TestPing() {
179177
assert.Equal(s.T(), 1, len(responses), "PingEmpty is an unary response")
180178
s.assertTrailerGrpcCode(trailers, codes.OK, "")
181179
s.assertHeadersContainMetadata(headers, expectedHeaders)
182-
s.assertHeadersContainMetadata(trailers, expectedTrailers)
180+
s.assertTrailersContainMetadata(trailers, expectedTrailers)
183181
s.assertHeadersContainCorsExpectedHeaders(headers, expectedHeaders)
184182
}
185183

@@ -195,7 +193,7 @@ func (s *GrpcWebWrapperTestSuite) TestPingError_WithTrailersInData() {
195193
assert.Equal(s.T(), 0, len(responses), "PingError is an unary response that has no payload")
196194
s.assertTrailerGrpcCode(trailers, codes.Unimplemented, "Not implemented PingError")
197195
s.assertHeadersContainMetadata(headers, expectedHeaders)
198-
s.assertHeadersContainMetadata(trailers, expectedTrailers)
196+
s.assertTrailersContainMetadata(trailers, expectedTrailers)
199197
s.assertHeadersContainCorsExpectedHeaders(headers, expectedHeaders)
200198
}
201199

@@ -209,7 +207,7 @@ func (s *GrpcWebWrapperTestSuite) TestPingError_WithTrailersInHeaders() {
209207
require.NoError(s.T(), err, "No error on making request")
210208

211209
assert.Equal(s.T(), 0, len(responses), "PingError is an unary response that has no payload")
212-
s.assertTrailerGrpcCode(headers, codes.Unimplemented, "Not implemented PingError")
210+
s.assertHeadersGrpcCode(headers, codes.Unimplemented, "Not implemented PingError")
213211
// s.assertHeadersContainMetadata(headers, expectedHeaders) // TODO(mwitkow): There is a bug in gRPC where headers don't get added if no payload exists.
214212
s.assertHeadersContainMetadata(headers, expectedTrailers)
215213
s.assertHeadersContainCorsExpectedHeaders(headers, expectedTrailers)
@@ -224,7 +222,7 @@ func (s *GrpcWebWrapperTestSuite) TestPingList() {
224222
assert.Equal(s.T(), expectedListResponses, len(responses), "the number of expected proto fields shouold match")
225223
s.assertTrailerGrpcCode(trailers, codes.OK, "")
226224
s.assertHeadersContainMetadata(headers, expectedHeaders)
227-
s.assertHeadersContainMetadata(trailers, expectedTrailers)
225+
s.assertTrailersContainMetadata(trailers, expectedTrailers)
228226
s.assertHeadersContainCorsExpectedHeaders(headers, expectedHeaders)
229227
}
230228

@@ -312,6 +310,14 @@ func (s *GrpcWebWrapperTestSuite) assertHeadersContainMetadata(headers http.Head
312310
}
313311
}
314312

313+
func (s *GrpcWebWrapperTestSuite) assertTrailersContainMetadata(trailers grpcweb.Trailer, meta metadata.MD) {
314+
for k, v := range meta {
315+
for _, vv := range v {
316+
assert.Equal(s.T(), trailers.Get(k), vv, "Expected there to be %v=%v", k, vv)
317+
}
318+
}
319+
}
320+
315321
func (s *GrpcWebWrapperTestSuite) assertHeadersContainCorsExpectedHeaders(headers http.Header, meta metadata.MD) {
316322
value := headers.Get("Access-Control-Expose-Headers")
317323
assert.NotEmpty(s.T(), value, "cors: access control expose headers should not be empty")
@@ -323,7 +329,15 @@ func (s *GrpcWebWrapperTestSuite) assertHeadersContainCorsExpectedHeaders(header
323329
}
324330
}
325331

326-
func (s *GrpcWebWrapperTestSuite) assertTrailerGrpcCode(trailers http.Header, code codes.Code, desc string) {
332+
func (s *GrpcWebWrapperTestSuite) assertHeadersGrpcCode(headers http.Header, code codes.Code, desc string) {
333+
require.NotEmpty(s.T(), headers.Get("grpc-status"), "grpc-status must not be empty in trailers")
334+
statusCode, err := strconv.Atoi(headers.Get("grpc-status"))
335+
require.NoError(s.T(), err, "no error parsing grpc-status")
336+
assert.EqualValues(s.T(), code, statusCode, "grpc-status must match expected code")
337+
assert.EqualValues(s.T(), desc, headers.Get("grpc-message"), "grpc-message is expected to match")
338+
}
339+
340+
func (s *GrpcWebWrapperTestSuite) assertTrailerGrpcCode(trailers grpcweb.Trailer, code codes.Code, desc string) {
327341
require.NotEmpty(s.T(), trailers.Get("grpc-status"), "grpc-status must not be empty in trailers")
328342
statusCode, err := strconv.Atoi(trailers.Get("grpc-status"))
329343
require.NoError(s.T(), err, "no error parsing grpc-status")
@@ -340,14 +354,40 @@ func serializeProtoMessages(messages []proto.Message) [][]byte {
340354
return out
341355
}
342356

343-
func readHeadersFromBytes(dataBytes []byte) http.Header {
357+
func readTrailersFromBytes(t *testing.T, dataBytes []byte) grpcweb.Trailer {
344358
bufferReader := bytes.NewBuffer(dataBytes)
345359
tp := textproto.NewReader(bufio.NewReader(bufferReader))
360+
361+
// First, read bytes as MIME headers.
362+
// However, it normalizes header names by textproto.CanonicalMIMEHeaderKey.
363+
// In the next step, replace header names by raw one.
346364
mimeHeader, err := tp.ReadMIMEHeader()
347365
if err == nil {
348-
return make(http.Header)
366+
return grpcweb.Trailer{}
367+
}
368+
369+
trailers := make(http.Header)
370+
bufferReader = bytes.NewBuffer(dataBytes)
371+
tp = textproto.NewReader(bufio.NewReader(bufferReader))
372+
373+
// Second, replace header names because gRPC Web trailer names must be lower-case.
374+
for {
375+
line, err := tp.ReadLine()
376+
if err == io.EOF {
377+
break
378+
}
379+
require.NoError(t, err, "failed to read header line")
380+
381+
i := strings.IndexByte(line, ':')
382+
if i == -1 {
383+
require.FailNow(t, "malformed header", line)
384+
}
385+
key := line[:i]
386+
if vv, ok := mimeHeader[textproto.CanonicalMIMEHeaderKey(key)]; ok {
387+
trailers[key] = vv
388+
}
349389
}
350-
return http.Header(mimeHeader)
390+
return grpcweb.HTTPTrailerToGrpcWebTrailer(trailers)
351391
}
352392

353393
func headerWithFlag(flags ...string) http.Header {

0 commit comments

Comments
 (0)