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  }