convert/unary.go
package convert
import (
"context"
"io/ioutil"
"net/http"
"strings"
"github.com/LLKennedy/mercury/httpapi"
"github.com/LLKennedy/mercury/logs"
"golang.org/x/net/websocket"
"google.golang.org/grpc"
"google.golang.org/grpc/codes"
"google.golang.org/grpc/status"
)
// ProxyRequest proxies an HTTP(S) or WS(S) request through a GRPC connection compliant with mercury/httpapi
func ProxyRequest(ctx context.Context, w http.ResponseWriter, r *http.Request, procedure string, conn grpc.ClientConnInterface, txid string, loggers ...logs.Writer) {
remote := httpapi.NewExposedServiceClient(conn)
isWebsocket := false
upgradeHader, ok := r.Header["Upgrade"]
if ok {
isWebsocket = len(upgradeHader) >= 1 && upgradeHader[0] == "websocket"
}
if isWebsocket {
// Stream request
handler := stream{
ctx: ctx,
remote: remote,
loggers: loggers,
procedure: procedure,
headers: r.Header,
txid: txid,
}
wssrv := &websocket.Server{
Handler: handler.Serve,
}
wssrv.ServeHTTP(w, r)
return
}
// Unary request
req := RequestFromRequest(r)
req.Procedure = procedure
bodyBytes, err := ioutil.ReadAll(r.Body)
req.Payload = bodyBytes
// Forward the actual GRPC request
var errStatus *status.Status
res, err := remote.ProxyUnary(ctx, req)
if err != nil {
// GRPC call failed, let's log it, process an error status
for _, logger := range loggers {
logger.LogErrorf(txid, "mercury: received error from target service: %v", err)
}
var ok bool
errStatus, ok = status.FromError(err)
if !ok {
// Can't get proper status code, return bad gateway
w.WriteHeader(http.StatusBadGateway)
} else {
w.WriteHeader(GRPCStatusToHTTPStatusCode(errStatus.Code()))
}
} else {
// No grpc error, get (presumably) success code from response
w.WriteHeader(int(res.GetStatusCode()))
}
// Write response body
for name, values := range res.GetWriteHeaders() {
for _, value := range values.GetValues() {
w.Header().Add(name, value)
}
}
if errStatus == nil {
if len(res.GetPayload()) < 1 {
w.Write([]byte("{}"))
} else {
w.Write(res.GetPayload())
}
} else {
w.Write([]byte(errStatus.Message()))
}
return
}
// RequestFromRequest creates a *httpapi.Request from *http.Request filling all values except body, which could error
func RequestFromRequest(r *http.Request) *httpapi.Request {
req := &httpapi.Request{}
req.Headers = map[string]*httpapi.MultiVal{}
for name, values := range r.Header {
newHeader := &httpapi.MultiVal{}
newHeader.Values = values
req.Headers[name] = newHeader
}
req.Method = MethodFromString(r.Method)
req.Params = map[string]*httpapi.MultiVal{}
for name, values := range r.URL.Query() {
newParam := &httpapi.MultiVal{}
newParam.Values = values
req.Params[name] = newParam
}
return req
}
// GRPCStatusToHTTPStatusCode converts a GRPC status to an HTTP Status Code
func GRPCStatusToHTTPStatusCode(grpcStatus codes.Code) (c int) {
c = http.StatusInternalServerError // Default to internal error in case something goes wrong
switch grpcStatus {
case codes.Aborted:
c = http.StatusBadGateway
case codes.AlreadyExists:
c = http.StatusNotModified
case codes.Canceled:
c = http.StatusGatewayTimeout
case codes.DataLoss:
c = http.StatusBadGateway
case codes.DeadlineExceeded:
c = http.StatusGatewayTimeout
case codes.FailedPrecondition:
c = http.StatusPreconditionFailed
case codes.Internal:
c = http.StatusBadGateway
case codes.InvalidArgument:
c = http.StatusBadRequest
case codes.NotFound:
c = http.StatusNotFound
case codes.OK:
c = http.StatusOK
case codes.OutOfRange:
c = http.StatusBadRequest
case codes.PermissionDenied:
c = http.StatusForbidden
case codes.ResourceExhausted:
c = http.StatusTooManyRequests
case codes.Unauthenticated:
c = http.StatusUnauthorized
case codes.Unavailable:
c = http.StatusServiceUnavailable
case codes.Unimplemented:
c = http.StatusNotImplemented
case codes.Unknown:
c = http.StatusBadGateway
default:
c = http.StatusInternalServerError
}
return
}
// MethodFromString converts a method string to a httpapi.Method
func MethodFromString(methodString string) httpapi.Method {
switch strings.ToUpper(methodString) {
case "GET":
return httpapi.Method_GET
case "HEAD":
return httpapi.Method_HEAD
case "POST":
return httpapi.Method_POST
case "PUT":
return httpapi.Method_PUT
case "DELETE":
return httpapi.Method_DELETE
case "CONNECT":
return httpapi.Method_CONNECT
case "OPTIONS":
return httpapi.Method_OPTIONS
case "TRACE":
return httpapi.Method_TRACE
case "PATCH":
return httpapi.Method_PATCH
default:
return httpapi.Method_UNKNOWN
}
}