caddy: Reverse proxying GRPC calls does not work
1. What version of Caddy are you running (caddy -version)?
Caddy 0.9.5
2. What are you trying to do?
I am trying to use caddy as a reverse proxy to a GRPC server.
3. What is your entire Caddyfile?
See 7. below.
4. How did you run Caddy (give the full command and describe the execution environment)?
See 7. below.
5. What did you expect to see?
Working GRPC calls.
6. What did you see instead (give full error messages and/or log)?
No errors or log output of any kind. The grpc-server never receives anything from caddy.
The client only sees that the connection was closed by the (caddy) server before any sensible data could be sent or received.
I have sniffed the traffic using wireshark. The entire communication of one call consists of the following steps:
- TCP SYN+ACK Client <-> Server
- HTTP/2 SETTINGS package Client -> Server
- TCP FIN+ACK Client <-> Server (FIN initiated by the sever)
It seems like caddy doesn’t know what to do with a SETTINGS package or doesn’t support something requested in that package and just terminates the connection.
I know my way around HTTP/1 a little but I don’t really know anything about HTTP/2 or SETTINGS packages. The only thing I found out is that a SETTINGS package does not contain any request (GET/POST etc.) and thus no URL. So caddy can’t know to which vserver/middleware it should route the package, thus the problem is most likely not in the proxy middleware but somewhere in the initial connection handling. I haven’t looked at the caddy code yet though. I will try to come around to doing that soon.
I have researched GRPC proxing and caddy before and I found these related issues. I have created a new one though because I would consider them related but different enough from this to justify a new issue.
- #631 Apparently grpc proxing does not work out-of-the-box with go1.6
- #54 This seems to ask for a far larger amount of features. I don’t need any fancy REST <-> GRPC converter, I simply want to have a caddy server on ports 80/443 and at the same time want to be able to have a grpc server on those ports.
7. How can someone who is starting from scratch reproduce this behavior as minimally as possible?
I have created a little test suite in go, which mostly automates testing if GRPC works. See this repo for the source code, including a Caddyfile with which I tested this.
It provides a go package where you can run go test -v on and it will tell you if everything is working or not.
About this issue
- Original URL
- State: closed
- Created 7 years ago
- Comments: 20 (7 by maintainers)
Commits related to this issue
- Fixed #1502: Proxying of unannounced trailers — committed to caddyserver/caddy by lhecker 7 years ago
- Fixed #1502: Proxying of unannounced trailers — committed to caddyserver/caddy by lhecker 7 years ago
- Fixed #1502: Proxying of unannounced trailers — committed to caddyserver/caddy by lhecker 7 years ago
- Fixed #1502: Proxying of unannounced trailers — committed to caddyserver/caddy by lhecker 7 years ago
- proxy: Fixed #1502: Proxying of unannounced trailers (#1588) — committed to caddyserver/caddy by lhecker 7 years ago
@mholt @targodan I’m currently in the process of moving which is why I’m so short on time regarding this bug.
Either I did something wrong when testing your code or you indeed did something differently or wrong. Here’s what I did:
NOTE: With Go you should try and set up a workspace and always use fully specified import paths instead of relative ones.
caddycaddyGrpcTestResults in:
Results in:
Did I do something wrong here?
Sorry, I made the mistake. 🙈
Thank you @lhecker for your work and the detailed information on how you tested it.
I compiled caddy outside of the gopath wich propably caused it to use the not-updated version from my gopath for any imports.
The fix works. 😄
PS: I didn’t close the issue yet because I don’t know if you guys keep issues open until the release is out or not. As far as I am concerned it can be closed.
This problem is caused by gRPC’s server always sending HTTP/2 frames in a certain fixed pattern. The gRPC client furthermore even relies on that particular behavior. If there’s a non-official gRPC solution at either end or in between (like Caddy) it ultimately breaks down.
The gRPC server sends the following frames:
Caddy then proxies the above to the gRPC client which receives the following:
As you might guess it’s exactly the same in the eyes of the HTTP/2 protocol, since the
HEADERSare empty and the flags are equivalent. So yeah… The gRPC libs simply implement HTTP/2 quite poorly and thus fail to handle Caddy’s frame ordering. 🙄 (BTW: It also fails with other proxies like nghttp2).You can fix your test repo for instance by deleting this code and replacing it with this:
This will use grpc-go’s
ServeHTTPmethod instead, which uses the proper HTTP/2 implementation of Go’s stdlib. The downside is that it’s quite a bit slower thanServe(relatively - it’s still multiple magnitudes faster than e.g. REST APIs).I’ll try to see if we can tweak our code somehow to make it work out of the box though. But I’m not entirely sure yet if that’s possible since we don’t control the actual HTTP/2 framing.
Alright I found the exact reason why Caddy fails (if someone else wants to debug this as well)… This is the response Caddy receives from the gRPC server:
The issue now is that there is no god damn “Trailer” header coming from the gRPC server! This causes our code to not be aware that any trailers are coming:
res.Trailerisnilandlen(res.Trailer)will thus return0. Since we then don’t set the trailer header Go will then silently ignore whatever we set at the end.If I use
ServeHTTPin the gRPC server instead (which again causes it to use the official Go HTTP/2 implementation) it looks a bit different:Please note the 2 log lines I marked with a star. Apart from that it’s nearly identical to the log above.
I think this issue should stay open since it’d be awesome if gRPC worked out of the box, but I’m not sure how to solve this. Or do you, @mholt?