github.com/renbou/grpcbridge@v0.0.2-0.20240416012907-bcbd8b12648a/webbridge/http.go (about) 1 package webbridge 2 3 import ( 4 "context" 5 "io" 6 "net/http" 7 8 "github.com/renbou/grpcbridge/bridgelog" 9 "github.com/renbou/grpcbridge/grpcadapter" 10 "github.com/renbou/grpcbridge/routing" 11 "github.com/renbou/grpcbridge/transcoding" 12 "google.golang.org/grpc/codes" 13 "google.golang.org/grpc/status" 14 "google.golang.org/protobuf/proto" 15 ) 16 17 // TranscodedHTTPBridgeOpts define all the optional settings which can be set for [TranscodedHTTPBridge]. 18 type TranscodedHTTPBridgeOpts struct { 19 // Logs are discarded by default. 20 Logger bridgelog.Logger 21 } 22 23 func (o TranscodedHTTPBridgeOpts) withDefaults() TranscodedHTTPBridgeOpts { 24 if o.Logger == nil { 25 o.Logger = bridgelog.Discard() 26 } 27 28 return o 29 } 30 31 // TranscodedHTTPBridge is a gRPC bridge which performs transcoding between HTTP and gRPC requests/responses 32 // using the specified transcoder, which isn't an optional argument by default since a single transcoder should be used 33 // for the various available bridges for compatibility between them. 34 // 35 // Currently, only unary RPCs are supported, and the streaming functionality of the transcoder is not used. 36 // TranscodedHTTPBridge performs transcoding not only for the request and response messages, 37 // but also for the errors and statuses returned by the router, transcoder, and gRPC connection to which the RPC is bridged. 38 // More specifically, gRPC status codes will be used to set an HTTP code according to the [Closest HTTP Mapping], 39 // with the possibility to override the code by returning an error implementing interface{ HTTPStatus() int }. 40 // 41 // Unary RPCs follow the Proto3 "all fields are optional" convention, treating completely-empty request messages as valid. 42 // This matches with gRPC-Gateway's behaviour. 43 // 44 // [Closest HTTP Mapping]: https://chromium.googlesource.com/external/github.com/grpc/grpc/+/refs/tags/v1.21.4-pre1/doc/statuscodes.md 45 type TranscodedHTTPBridge struct { 46 logger bridgelog.Logger 47 router routing.HTTPRouter 48 transcoder transcoding.HTTPTranscoder 49 } 50 51 // NewTranscodedHTTPBridge initializes a new [TranscodedHTTPBridge] using the specified router and transcoder. 52 func NewTranscodedHTTPBridge(router routing.HTTPRouter, transcoder transcoding.HTTPTranscoder, opts TranscodedHTTPBridgeOpts) *TranscodedHTTPBridge { 53 opts = opts.withDefaults() 54 55 return &TranscodedHTTPBridge{ 56 logger: opts.Logger, 57 router: router, 58 transcoder: transcoder, 59 } 60 } 61 62 // ServeHTTP implements [net/http.Handler] so that the bridge is used as a normal HTTP handler. 63 func (b *TranscodedHTTPBridge) ServeHTTP(unwrappedRW http.ResponseWriter, r *http.Request) { 64 w := &responseWrapper{ResponseWriter: unwrappedRW} 65 66 conn, route, err := b.router.RouteHTTP(r) 67 if err != nil { 68 writeError(w, r, nil, err) 69 return 70 } 71 72 reqtc, resptc, err := b.transcoder.Bind(transcoding.HTTPRequest{ 73 Target: route.Target, 74 Service: route.Service, 75 Method: route.Method, 76 Binding: route.Binding, 77 RawRequest: r, 78 PathParams: route.PathParams, 79 }) 80 if err != nil { 81 writeError(w, r, nil, err) 82 return 83 } 84 85 // At this point all responses including errors should be transcoded to get properly parsed by a client. 86 outgoing, err := conn.Stream(r.Context(), route.Method.RPCName) 87 if err != nil { 88 writeError(w, r, resptc, err) 89 return 90 } 91 92 err = grpcadapter.ForwardServerToClient(r.Context(), grpcadapter.ForwardS2C{ 93 Incoming: &unaryHTTPStream{w: w, r: r, reqtc: reqtc, resptc: resptc, readCh: make(chan struct{})}, 94 Outgoing: outgoing, 95 Method: route.Method, 96 }) 97 if err != nil { 98 writeError(w, r, resptc, err) 99 } 100 } 101 102 type unaryHTTPStream struct { 103 w *responseWrapper 104 r *http.Request 105 reqtc transcoding.HTTPRequestTranscoder 106 resptc transcoding.HTTPResponseTranscoder 107 read bool 108 readCh chan struct{} 109 sent bool 110 } 111 112 func (s *unaryHTTPStream) Send(_ context.Context, msg proto.Message) error { 113 if s.sent { 114 return status.Error(codes.Internal, "grpcbridge: tried sending second response on unary stream") 115 } 116 117 // Wait for request to be sent, because after this we aren't guaranteed to be able to read the request body, 118 // for example when using HTTP/1.1. See http.ResponseWriter.Write comment for more info. 119 <-s.readCh 120 121 b, err := s.resptc.Transcode(msg) 122 if err != nil { 123 return responseTranscodingError(err) 124 } 125 126 // Set here, because Write can perform a partial write and still return an error 127 s.sent = true 128 129 s.w.Header()[contentTypeHeader] = []string{s.resptc.ContentType(msg)} 130 _, err = s.w.Write(b) 131 return err 132 } 133 134 func (s *unaryHTTPStream) Recv(_ context.Context, msg proto.Message) error { 135 if s.read { 136 return io.EOF 137 } 138 139 b, err := io.ReadAll(s.r.Body) 140 if err != nil { 141 return status.Errorf(codes.InvalidArgument, "failed to read request body: %s", err) 142 } 143 144 s.read = true 145 close(s.readCh) 146 147 // Treat completely empty bodies as valid ones. 148 if len(b) == 0 { 149 return nil 150 } 151 152 return requestTranscodingError(s.reqtc.Transcode(b, msg)) 153 }