grpc / grpc-dart

The Dart language implementation of gRPC.
https://pub.dev/packages/grpc
Apache License 2.0
861 stars 271 forks source link

OPTIONS method used when metadata set, but POST method used otherwise #742

Closed dbblackdiamond closed 1 month ago

dbblackdiamond commented 1 month ago

Hi,

I am building a multi-platform application using Flutter. The app will be available on Android, IOS and web. I have migrated the backend over to gRPC and the Android and IOS versions work fine with that, but not the web. To get the web version working, I implemented Envoy sidecar on all of my services and it is working for the most part. I am using the GrpcOrGrpcWebClientChannel.toSingleEndpoint method to create a channel to my services. One of the services requires me to send a custom header authorization with a JWT token. When I create my stub using the ServiceClient method, in the CallOptions, I define metadata: {'authorization': token}. When I do this though, the underlying RPC call is done using the OPTIONS method, but when I remove that line, it is done using the POST method as show below. With the metadata line: this is what I see at my ingress gateway:

x.x.x.x - - [04/Oct/2024:19:36:40 +0000] "OPTIONS /jackpots.v1.JackpotService/GetJackpotsForRegionForStatus HTTP/2.0" 200 0 "http://localhost:39067/" "Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/128.0.0.0 Safari/537.36" 111 0.007 [hustlinhome-dev-jackpot-service-26197] [] 10.244.0.66:26197 0 0.006 200 97bafcdebc79ea53a65c5c63fbca9910

and at the envoy sidecar:

[2024-10-04 19:36:40.601][18][debug][http] [source/common/http/conn_manager_impl.cc:1135] [Tags: "ConnectionId":"0","StreamId":"4370708096276452121"] request headers complete (end_stream=true):
':method', 'OPTIONS'
':scheme', 'http'
':path', '/jackpots.v1.JackpotService/GetJackpotsForRegionForStatus'
':authority', 'upstream_balancer'
'x-request-id', '97bafcdebc79ea53a65c5c63fbca9910'
'x-real-ip', '66.222.150.59'
'x-forwarded-for', '66.222.150.59'
'x-forwarded-host', 'dev.hustlinhome.com'
'x-forwarded-port', '443'
'x-forwarded-proto', 'https'
'x-forwarded-scheme', 'https'
'x-scheme', 'https'
'accept', '*/*'
'access-control-request-method', 'POST'
'access-control-request-headers', 'authorization,content-type,x-grpc-web,x-user-agent'
'origin', 'http://localhost:39067'
'user-agent', 'Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/128.0.0.0 Safari/537.36'
'sec-fetch-mode', 'cors'
'sec-fetch-site', 'cross-site'
'sec-fetch-dest', 'empty'
'referer', 'http://localhost:39067/'
'accept-encoding', 'gzip, deflate, br, zstd'
'accept-language', 'en-GB,en-US;q=0.9,en;q=0.8'
'priority', 'u=1, i'

but without the metadata line: this is what I see at my ingress gateway:

x.x.x.x - - [04/Oct/2024:20:03:50 +0000] "POST /jackpots.v1.JackpotService/GetJackpotsForRegisteredRiderForStatus HTTP/2.0" 200 0 "http://localhost:39067/" "Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/128.0.0.0 Safari/537.36" 120 0.435 [hustlinhome-dev-jackpot-service-26197] [] 10.244.0.66:26197 0 0.435 200 e02a18a8f6c8b0e891f5554e6489a216

and at the envoy sidecar:

[2024-10-04 20:03:49.541][16][debug][http] [source/common/http/conn_manager_impl.cc:1135] [Tags: "ConnectionId":"5","StreamId":"4422272411264714149"] request headers complete (end_stream=false):
':method', 'POST'
':scheme', 'http'
':path', '/jackpots.v1.JackpotService/GetJackpotsForRegionForStatus'
':authority', 'upstream_balancer'
'x-request-id', '2a4f84d2a2646dc90b311d4241a17bc2'
'x-real-ip', '66.222.150.59'
'x-forwarded-for', '66.222.150.59'
'x-forwarded-host', 'dev.hustlinhome.com'
'x-forwarded-port', '443'
'x-forwarded-proto', 'https'
'x-forwarded-scheme', 'https'
'x-scheme', 'https'
'content-length', '36'
'sec-ch-ua', '"Chromium";v="128", "Not;A=Brand";v="24", "Google Chrome";v="128"'
'x-user-agent', 'grpc-web-dart/0.1'
'x-grpc-web', '1'
'sec-ch-ua-platform', '"Linux"'
'sec-ch-ua-mobile', '?0'
'user-agent', 'Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/128.0.0.0 Safari/537.36'
'content-type', 'application/grpc-web+proto'
'accept', '*/*'
'origin', 'http://localhost:39067'
'sec-fetch-site', 'cross-site'
'sec-fetch-mode', 'cors'
'sec-fetch-dest', 'empty'
'referer', 'http://localhost:39067/'
'accept-encoding', 'gzip, deflate, br, zstd'
'accept-language', 'en-GB,en-US;q=0.9,en;q=0.8'
'priority', 'u=1, i'

The only difference between the 2 is the presence of the metadata definition in CallOptions. By the way, on Android, whether the line is there or not, the call is made using POST method.

I am running grpc package version 4.0.1.

My question is: Why does the grpc-web implementation use OPTIONS vs POST when custom headers in metadata are defined?

This is causing an issue where all the calls with metadata are failing, but all the calls without it are working fine.

Thanks a lot in advance, Bertrand.

mraleph commented 1 month ago

This OPTIONS request is a so called CORS pre-flight request. It happens because browser has to ask the server if authorization is an allowed header via Access-Control-Request-Headers.

You can avoid that by setting WebCallOptions.bypassCorsPreflight to true which tells grpc-web to pack metadata into $httpHeaders query parameter instead of sending it as headers. You will of course need to properly handle this in Envoy though.