github.com/renbou/grpcbridge@v0.0.2-0.20240416012907-bcbd8b12648a/webbridge/webbridge.go (about)

     1  // Package webbridge contains implementations of various handlers for bridging web-originated requests to gRPC.
     2  // It allows using gRPC-only services through all kinds of interfaces supporting all the possible streaming variants.
     3  //
     4  // The available functionality can be separated into two different kinds by the API format:
     5  //   - Typical REST-like API implemented using classic single-request-single-response and streamed HTTP requests,
     6  //     streaming WebSocket connections, and Server-Sent Events, all coming with support for request path parameters,
     7  //     query parameters, and custom body path specification.
     8  //   - Modern gRPC-Web API supporting both unary and streaming RPCs for HTTP and WebSocket requests,
     9  //     with WebTransport support planned, too.
    10  package webbridge
    11  
    12  import (
    13  	"bytes"
    14  	"fmt"
    15  	"net/http"
    16  
    17  	"github.com/grpc-ecosystem/grpc-gateway/v2/runtime"
    18  	"github.com/renbou/grpcbridge/transcoding"
    19  	"google.golang.org/grpc/codes"
    20  	"google.golang.org/grpc/status"
    21  )
    22  
    23  var (
    24  	contentTypeHeader        = http.CanonicalHeaderKey("Content-Type")
    25  	contentTypeOptionsHeader = http.CanonicalHeaderKey("X-Content-Type-Options")
    26  )
    27  
    28  const httpStatusCanceled = 499
    29  
    30  // responseWrapper wraps a ResponseWriter to avoid error handling when the response has already been partially written.
    31  type responseWrapper struct {
    32  	http.ResponseWriter
    33  	writtenStatus bool
    34  }
    35  
    36  func (w *responseWrapper) WriteHeader(statusCode int) {
    37  	w.writtenStatus = true
    38  	w.ResponseWriter.WriteHeader(statusCode)
    39  }
    40  
    41  func (w *responseWrapper) Write(data []byte) (int, error) {
    42  	w.writtenStatus = true
    43  	return w.ResponseWriter.Write(data)
    44  }
    45  
    46  func errorStatus(err error) (*status.Status, int) {
    47  	st := status.Convert(err)
    48  
    49  	respStatus := runtime.HTTPStatusFromCode(st.Code())
    50  	if s, ok := err.(interface{ HTTPStatus() int }); ok {
    51  		respStatus = s.HTTPStatus()
    52  	}
    53  
    54  	return st, respStatus
    55  }
    56  
    57  func writeTextError(w *responseWrapper, err error) {
    58  	st, respStatus := errorStatus(err)
    59  	http.Error(w, st.Message(), respStatus)
    60  }
    61  
    62  func transcodeError(w *responseWrapper, t transcoding.HTTPResponseTranscoder, err error) {
    63  	st, respStatus := errorStatus(err)
    64  	stProto := st.Proto()
    65  
    66  	respData, transcodeErr := t.Transcode(stProto)
    67  	if transcodeErr == nil {
    68  		w.Header()[contentTypeHeader] = []string{t.ContentType(stProto)}
    69  	} else {
    70  		var buf bytes.Buffer
    71  		fmt.Fprintf(&buf, "unable to transcode response status code = %s desc = %s: %s\n", st.Code(), st.Message(), transcodeErr)
    72  
    73  		// copied from http.Error function, seems safe
    74  		w.Header()[contentTypeHeader] = []string{"text/plain; charset=utf-8"}
    75  		w.Header()[contentTypeOptionsHeader] = []string{"nosniff"}
    76  	}
    77  
    78  	w.WriteHeader(respStatus)
    79  	w.Write(respData)
    80  }
    81  
    82  func writeError(w *responseWrapper, r *http.Request, t transcoding.HTTPResponseTranscoder, err error) {
    83  	if w.writtenStatus {
    84  		// No point in writing an error when the response has already been partially received, it would be meaningless
    85  		// (wrong status, wrong content-type, unexpected mixing of different message formats, etc).
    86  		return
    87  	}
    88  
    89  	if requestCanceled(r) {
    90  		// Avoid wasting more resources if client request is not waiting.
    91  		w.WriteHeader(httpStatusCanceled)
    92  		return
    93  	}
    94  
    95  	if t == nil {
    96  		writeTextError(w, err)
    97  		return
    98  	}
    99  
   100  	transcodeError(w, t, err)
   101  }
   102  
   103  func requestCanceled(r *http.Request) bool {
   104  	select {
   105  	case <-r.Context().Done():
   106  		return true
   107  	default:
   108  		return false
   109  	}
   110  }
   111  
   112  // requestTranscodingError should be used to convert non-status errors received from a request transcoder.
   113  func requestTranscodingError(err error) error {
   114  	return wrapTranscodingError(err, codes.InvalidArgument)
   115  }
   116  
   117  // responseTranscodingError should be used to convert non-status errors received from a response transcoder.
   118  func responseTranscodingError(err error) error {
   119  	return wrapTranscodingError(err, codes.Internal)
   120  }
   121  
   122  func wrapTranscodingError(err error, defaultCode codes.Code) error {
   123  	type grpcstatus interface{ GRPCStatus() *status.Status }
   124  
   125  	if err == nil {
   126  		return nil
   127  	} else if _, ok := err.(grpcstatus); ok {
   128  		return err
   129  	}
   130  
   131  	return status.Error(defaultCode, err.Error())
   132  }