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 }