Skip to content

Commit 6661e26

Browse files
Hixon10vikin91
andauthored
Add an option to override content type for a client (#132)
Co-authored-by: Piotr Rygielski <[email protected]>
1 parent d52884b commit 6661e26

File tree

4 files changed

+88
-5
lines changed

4 files changed

+88
-5
lines changed

_integration-tests/echo_service_test.go

+53
Original file line numberDiff line numberDiff line change
@@ -192,6 +192,50 @@ func testWithEchoService(t *testing.T, serverPreferGRPCWeb bool) {
192192
expectClientStreamOK: false,
193193
expectBidiStreamOK: false,
194194
},
195+
{
196+
targetID: "downgrading-grpc",
197+
behindHTTP1ReverseProxy: false,
198+
useProxy: true,
199+
forceDowngrade: true,
200+
customContentType: "application/grpc-web",
201+
expectUnaryOK: true,
202+
expectServerStreamOK: true,
203+
expectClientStreamOK: false,
204+
expectBidiStreamOK: false,
205+
},
206+
{
207+
targetID: "downgrading-grpc",
208+
behindHTTP1ReverseProxy: true,
209+
useProxy: true,
210+
forceDowngrade: true,
211+
customContentType: "application/grpc-web",
212+
expectUnaryOK: true,
213+
expectServerStreamOK: true,
214+
expectClientStreamOK: false,
215+
expectBidiStreamOK: false,
216+
},
217+
{
218+
targetID: "downgrading-grpc",
219+
behindHTTP1ReverseProxy: true,
220+
useProxy: true,
221+
forceDowngrade: false,
222+
customContentType: "application/grpc-web",
223+
expectUnaryOK: true,
224+
expectServerStreamOK: true,
225+
expectClientStreamOK: false,
226+
expectBidiStreamOK: false,
227+
},
228+
{
229+
targetID: "downgrading-grpc",
230+
behindHTTP1ReverseProxy: true,
231+
useProxy: true,
232+
forceDowngrade: true,
233+
customContentType: "dummy",
234+
expectUnaryOK: false,
235+
expectServerStreamOK: false,
236+
expectClientStreamOK: false,
237+
expectBidiStreamOK: false,
238+
},
195239
}
196240

197241
for _, c := range cases {
@@ -317,6 +361,7 @@ type testCase struct {
317361
useProxy bool
318362
useWebSocket bool
319363
forceDowngrade bool
364+
customContentType string
320365

321366
expectUnaryOK bool
322367
expectClientStreamOK bool
@@ -343,6 +388,10 @@ func (c *testCase) Name() string {
343388
} else {
344389
sb.WriteString("-direct")
345390
}
391+
392+
if len(c.customContentType) > 0 {
393+
sb.WriteString("-custom-content-type")
394+
}
346395
return sb.String()
347396
}
348397

@@ -381,6 +430,10 @@ func (c *testCase) Run(t *testing.T, cfg *testConfig) {
381430
}
382431
opts = append(opts, client.UseWebSocket(c.useWebSocket), client.ForceDowngrade(c.forceDowngrade))
383432

433+
if len(c.customContentType) > 0 {
434+
opts = append(opts, client.WithContentType(c.customContentType))
435+
}
436+
384437
cc, err = client.ConnectViaProxy(ctx, targetAddr, nil, opts...)
385438
} else {
386439
cc, err = grpc.DialContext(ctx, targetAddr, grpc.WithTransportCredentials(insecure.NewCredentials()))

client/options.go

+13
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,7 @@ type connectOptions struct {
2222
forceHTTP2 bool
2323
forceDowngrade bool
2424
useWebSocket bool
25+
contentType string
2526
}
2627

2728
// ConnectOption is an option that can be passed to the `ConnectViaProxy` method.
@@ -66,6 +67,12 @@ func ForceDowngrade(force bool) ConnectOption {
6667
return forceDowngradeOption(force)
6768
}
6869

70+
// WithContentType returns a connection option that instructs the
71+
// client to use a custom content type for sending requests to the server.
72+
func WithContentType(contentType string) ConnectOption {
73+
return contentTypeOption(contentType)
74+
}
75+
6976
type dialOptsOption []grpc.DialOption
7077

7178
func (o dialOptsOption) apply(opts *connectOptions) {
@@ -95,3 +102,9 @@ type forceDowngradeOption bool
95102
func (o forceDowngradeOption) apply(opts *connectOptions) {
96103
opts.forceDowngrade = bool(o)
97104
}
105+
106+
type contentTypeOption string
107+
108+
func (o contentTypeOption) apply(opts *connectOptions) {
109+
opts.contentType = string(o)
110+
}

client/proxy.go

+12-4
Original file line numberDiff line numberDiff line change
@@ -79,7 +79,7 @@ func writeError(w http.ResponseWriter, err error) {
7979
w.Header().Set("Grpc-Message", grpcproto.EncodeGrpcMessage(errMsg))
8080
}
8181

82-
func createReverseProxy(endpoint string, transport http.RoundTripper, insecure, forceDowngrade bool) *httputil.ReverseProxy {
82+
func createReverseProxy(endpoint string, transport http.RoundTripper, insecure, forceDowngrade bool, contentType string) *httputil.ReverseProxy {
8383
scheme := "https"
8484
if insecure {
8585
scheme = "http"
@@ -95,6 +95,14 @@ func createReverseProxy(endpoint string, transport http.RoundTripper, insecure,
9595
req.Header.Add("Accept", "application/grpc")
9696
}
9797
req.Header.Add("Accept", "application/grpc-web")
98+
99+
if len(contentType) > 0 {
100+
// Replacing old content type (e.g., application/grpc), to an overridden content type.
101+
// Without removing old header, some gRPC-Web servers will not work,
102+
// because an HTTP client will send both old and new header values.
103+
req.Header.Set("Content-Type", contentType)
104+
}
105+
98106
req.URL.Scheme = scheme
99107
req.URL.Host = endpoint
100108
},
@@ -142,12 +150,12 @@ func createTransport(tlsClientConf *tls.Config, forceHTTP2 bool, extraH2ALPNs []
142150
return transport, nil
143151
}
144152

145-
func createClientProxy(endpoint string, tlsClientConf *tls.Config, forceHTTP2, forceDowngrade bool, extraH2ALPNs []string) (*http.Server, pipeconn.DialContextFunc, error) {
153+
func createClientProxy(endpoint string, tlsClientConf *tls.Config, forceHTTP2, forceDowngrade bool, extraH2ALPNs []string, contentType string) (*http.Server, pipeconn.DialContextFunc, error) {
146154
transport, err := createTransport(tlsClientConf, forceHTTP2, extraH2ALPNs)
147155
if err != nil {
148156
return nil, nil, errors.Wrap(err, "creating transport")
149157
}
150-
proxy := createReverseProxy(endpoint, transport, tlsClientConf == nil, forceDowngrade)
158+
proxy := createReverseProxy(endpoint, transport, tlsClientConf == nil, forceDowngrade, contentType)
151159
return makeProxyServer(proxy)
152160
}
153161

@@ -171,7 +179,7 @@ func ConnectViaProxy(ctx context.Context, endpoint string, tlsClientConf *tls.Co
171179
if connectOpts.useWebSocket {
172180
proxy, dialCtx, err = createClientWSProxy(endpoint, tlsClientConf)
173181
} else {
174-
proxy, dialCtx, err = createClientProxy(endpoint, tlsClientConf, connectOpts.forceHTTP2, connectOpts.forceDowngrade, connectOpts.extraH2ALPNs)
182+
proxy, dialCtx, err = createClientProxy(endpoint, tlsClientConf, connectOpts.forceHTTP2, connectOpts.forceDowngrade, connectOpts.extraH2ALPNs, connectOpts.contentType)
175183
}
176184

177185
if err != nil {

server/server.go

+10-1
Original file line numberDiff line numberDiff line change
@@ -182,16 +182,25 @@ func CreateDowngradingHandler(grpcSrv *grpc.Server, httpHandler http.Handler, op
182182
return
183183
}
184184

185-
if contentType, _ := stringutils.Split2(req.Header.Get("Content-Type"), "+"); contentType != "application/grpc" {
185+
if !isContentTypeValid(req.Header.Get("Content-Type")) {
186186
// Non-gRPC request to the same port.
187187
httpHandler.ServeHTTP(w, req)
188188
return
189189
}
190190

191+
// Internally content type must be application/grpc,
192+
// See: https://github.com/grpc/grpc-go/blob/9deee9b/internal/grpcutil/method.go#L61
193+
req.Header.Set("Content-Type", "application/grpc")
194+
191195
handleGRPCWeb(w, req, validGRPCWebPaths, grpcSrv, &serverOpts)
192196
})
193197
}
194198

199+
func isContentTypeValid(contentType string) bool {
200+
ct, _ := stringutils.Split2(contentType, "+")
201+
return ct == "application/grpc" || ct == "application/grpc-web"
202+
}
203+
195204
func isWebSocketUpgrade(header http.Header) (bool, error) {
196205
if header.Get("Sec-Websocket-Protocol") != grpcwebsocket.SubprotocolName {
197206
return false, nil

0 commit comments

Comments
 (0)