github.com/tickoalcantara12/micro/v3@v3.0.0-20221007104245-9d75b9bcbab9/service/api/handler/rpc/rpc.go (about)

     1  // Copyright 2020 Asim Aslam
     2  //
     3  // Licensed under the Apache License, Version 2.0 (the "License");
     4  // you may not use this file except in compliance with the License.
     5  // You may obtain a copy of the License at
     6  //
     7  //     https://www.apache.org/licenses/LICENSE-2.0
     8  //
     9  // Unless required by applicable law or agreed to in writing, software
    10  // distributed under the License is distributed on an "AS IS" BASIS,
    11  // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
    12  // See the License for the specific language governing permissions and
    13  // limitations under the License.
    14  //
    15  // Original source: github.com/micro/go-micro/v3/api/handler/rpc/rpc.go
    16  
    17  // Package rpc is a go-micro rpc handler.
    18  package rpc
    19  
    20  import (
    21  	bts "bytes"
    22  	"encoding/json"
    23  	"io"
    24  	"net/http"
    25  	"strconv"
    26  	"strings"
    27  
    28  	"github.com/tickoalcantara12/micro/v3/service/api"
    29  	"github.com/tickoalcantara12/micro/v3/service/api/handler"
    30  	"github.com/tickoalcantara12/micro/v3/service/client"
    31  	"github.com/tickoalcantara12/micro/v3/service/errors"
    32  	"github.com/tickoalcantara12/micro/v3/service/logger"
    33  	"github.com/tickoalcantara12/micro/v3/util/codec/bytes"
    34  	"github.com/tickoalcantara12/micro/v3/util/ctx"
    35  )
    36  
    37  const (
    38  	Handler = "rpc"
    39  )
    40  
    41  var (
    42  	// supported json codecs
    43  	jsonCodecs = []string{
    44  		"application/grpc+json",
    45  		"application/json",
    46  		"application/json-rpc",
    47  	}
    48  
    49  	// support proto codecs
    50  	protoCodecs = []string{
    51  		"application/grpc",
    52  		"application/grpc+proto",
    53  		"application/proto",
    54  		"application/protobuf",
    55  		"application/proto-rpc",
    56  		"application/octet-stream",
    57  	}
    58  )
    59  
    60  type rpcHandler struct {
    61  	opts handler.Options
    62  	s    *api.Service
    63  }
    64  
    65  type buffer struct {
    66  	io.ReadCloser
    67  }
    68  
    69  func (b *buffer) Write(_ []byte) (int, error) {
    70  	return 0, nil
    71  }
    72  
    73  // see https://stackoverflow.com/questions/28595664/how-to-stop-json-marshal-from-escaping-and/28596225
    74  func jsonMarshal(t interface{}) ([]byte, error) {
    75  	buffer := &bts.Buffer{}
    76  	encoder := json.NewEncoder(buffer)
    77  	encoder.SetEscapeHTML(false)
    78  	err := encoder.Encode(t)
    79  	return bts.TrimRight(buffer.Bytes(), "\n"), err
    80  }
    81  
    82  func (h *rpcHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) {
    83  	bsize := handler.DefaultMaxRecvSize
    84  	if h.opts.MaxRecvSize > 0 {
    85  		bsize = h.opts.MaxRecvSize
    86  	}
    87  
    88  	r.Body = http.MaxBytesReader(w, r.Body, bsize)
    89  
    90  	defer r.Body.Close()
    91  	var service *api.Service
    92  	var c client.Client
    93  
    94  	if v, ok := r.Context().(handler.Context); ok {
    95  		// we were given the service
    96  		service = v.Service()
    97  		c = v.Client()
    98  	} else if h.opts.Router != nil {
    99  		// try get service from router
   100  		s, err := h.opts.Router.Route(r)
   101  		if err != nil {
   102  			writeError(w, r, errors.InternalServerError("go.micro.api", err.Error()))
   103  			return
   104  		}
   105  		service = s
   106  		c = h.opts.Client
   107  	} else {
   108  		// we have no way of routing the request
   109  		writeError(w, r, errors.InternalServerError("go.micro.api", "no route found"))
   110  		return
   111  	}
   112  
   113  	ct := r.Header.Get("Content-Type")
   114  
   115  	// Strip charset from Content-Type (like `application/json; charset=UTF-8`)
   116  	if idx := strings.IndexRune(ct, ';'); idx >= 0 {
   117  		ct = ct[:idx]
   118  	}
   119  
   120  	// create context
   121  	cx := ctx.FromRequest(r)
   122  
   123  	// set merged context to request
   124  	*r = *r.Clone(cx)
   125  	// if stream we currently only support json
   126  	if isStream(r, service) {
   127  		serveStream(cx, w, r, service, c)
   128  		return
   129  	}
   130  
   131  	// create custom router
   132  	var nodes []string
   133  	for _, service := range service.Services {
   134  		for _, node := range service.Nodes {
   135  			nodes = append(nodes, node.Address)
   136  		}
   137  	}
   138  	callOpt := client.WithAddress(nodes...)
   139  
   140  	// walk the standard call path
   141  	// get payload
   142  	br, err := api.RequestPayload(r)
   143  	if err != nil {
   144  		writeError(w, r, err)
   145  		return
   146  	}
   147  
   148  	var rsp []byte
   149  
   150  	switch {
   151  	// proto codecs
   152  	case hasCodec(ct, protoCodecs):
   153  		var request *bytes.Frame
   154  		// if the extracted payload isn't empty lets use it
   155  		if len(br) > 0 {
   156  			request = &bytes.Frame{Data: br}
   157  		}
   158  
   159  		// create the request
   160  		req := c.NewRequest(
   161  			service.Name,
   162  			service.Endpoint.Name,
   163  			request,
   164  			client.WithContentType(ct),
   165  		)
   166  
   167  		// make the call
   168  		var response *bytes.Frame
   169  		if err := c.Call(cx, req, response, callOpt); err != nil {
   170  			writeError(w, r, err)
   171  			return
   172  		}
   173  		rsp = response.Data
   174  	default:
   175  		// if json codec is not present set to json
   176  		if !hasCodec(ct, jsonCodecs) {
   177  			ct = "application/json"
   178  		}
   179  
   180  		// default to trying json
   181  		var request json.RawMessage
   182  		// if the extracted payload isn't empty lets use it
   183  		if len(br) > 0 {
   184  			request = json.RawMessage(br)
   185  		}
   186  
   187  		// create request/response
   188  		var response interface{}
   189  
   190  		req := c.NewRequest(
   191  			service.Name,
   192  			service.Endpoint.Name,
   193  			&request,
   194  			client.WithContentType(ct),
   195  		)
   196  		// make the call
   197  		if err := c.Call(cx, req, &response, callOpt); err != nil {
   198  			writeError(w, r, err)
   199  			return
   200  		}
   201  
   202  		// marshall response
   203  		// see https://play.golang.org/p/oBNxUjVTzus
   204  		rsp, err = jsonMarshal(response)
   205  		if err != nil {
   206  			writeError(w, r, err)
   207  			return
   208  		}
   209  	}
   210  
   211  	// write the response
   212  	writeResponse(w, r, rsp, ct)
   213  }
   214  
   215  func (rh *rpcHandler) String() string {
   216  	return "rpc"
   217  }
   218  
   219  func hasCodec(ct string, codecs []string) bool {
   220  	for _, codec := range codecs {
   221  		if ct == codec {
   222  			return true
   223  		}
   224  	}
   225  	return false
   226  }
   227  
   228  func writeError(w http.ResponseWriter, r *http.Request, err error) {
   229  	// response content type
   230  	w.Header().Set("Content-Type", "application/json")
   231  
   232  	// parse out the error code
   233  	ce := errors.Parse(err.Error())
   234  
   235  	switch ce.Code {
   236  	case 0:
   237  		// assuming it's totally screwed
   238  		ce.Code = 500
   239  		ce.Id = "go.micro.api"
   240  		ce.Status = http.StatusText(500)
   241  		ce.Detail = "error during request: " + ce.Detail
   242  		w.WriteHeader(500)
   243  	default:
   244  		w.WriteHeader(int(ce.Code))
   245  	}
   246  
   247  	// Set trailers
   248  	if strings.Contains(r.Header.Get("Content-Type"), "application/grpc") {
   249  		w.Header().Set("Trailer", "grpc-status")
   250  		w.Header().Set("Trailer", "grpc-message")
   251  		w.Header().Set("grpc-status", "13")
   252  		w.Header().Set("grpc-message", ce.Detail)
   253  	}
   254  
   255  	_, werr := w.Write([]byte(ce.Error()))
   256  	if werr != nil {
   257  		if logger.V(logger.ErrorLevel, logger.DefaultLogger) {
   258  			logger.Error(werr)
   259  		}
   260  	}
   261  }
   262  
   263  func writeResponse(w http.ResponseWriter, r *http.Request, rsp []byte, ct string) {
   264  	w.Header().Set("Content-Type", ct)
   265  	w.Header().Set("Content-Length", strconv.Itoa(len(rsp)))
   266  
   267  	// Set trailers
   268  	if strings.Contains(r.Header.Get("Content-Type"), "application/grpc") {
   269  		w.Header().Set("Trailer", "grpc-status")
   270  		w.Header().Set("Trailer", "grpc-message")
   271  		w.Header().Set("grpc-status", "0")
   272  		w.Header().Set("grpc-message", "")
   273  	}
   274  
   275  	// write 204 status if rsp is nil
   276  	if len(rsp) == 0 {
   277  		w.WriteHeader(http.StatusNoContent)
   278  	}
   279  
   280  	// write response
   281  	_, err := w.Write(rsp)
   282  	if err != nil {
   283  		if logger.V(logger.ErrorLevel, logger.DefaultLogger) {
   284  			logger.Error(err)
   285  		}
   286  	}
   287  
   288  }
   289  
   290  func NewHandler(opts ...handler.Option) handler.Handler {
   291  	options := handler.NewOptions(opts...)
   292  	return &rpcHandler{
   293  		opts: options,
   294  	}
   295  }
   296  
   297  func WithService(s *api.Service, opts ...handler.Option) handler.Handler {
   298  	options := handler.NewOptions(opts...)
   299  	return &rpcHandler{
   300  		opts: options,
   301  		s:    s,
   302  	}
   303  }