187 lines
6.5 KiB
Go
187 lines
6.5 KiB
Go
|
package runtime
|
||
|
|
||
|
import (
|
||
|
"context"
|
||
|
"io"
|
||
|
"net/http"
|
||
|
"strings"
|
||
|
|
||
|
"github.com/grpc-ecosystem/grpc-gateway/internal"
|
||
|
"google.golang.org/grpc/codes"
|
||
|
"google.golang.org/grpc/grpclog"
|
||
|
"google.golang.org/grpc/status"
|
||
|
)
|
||
|
|
||
|
// HTTPStatusFromCode converts a gRPC error code into the corresponding HTTP response status.
|
||
|
// See: https://github.com/googleapis/googleapis/blob/master/google/rpc/code.proto
|
||
|
func HTTPStatusFromCode(code codes.Code) int {
|
||
|
switch code {
|
||
|
case codes.OK:
|
||
|
return http.StatusOK
|
||
|
case codes.Canceled:
|
||
|
return http.StatusRequestTimeout
|
||
|
case codes.Unknown:
|
||
|
return http.StatusInternalServerError
|
||
|
case codes.InvalidArgument:
|
||
|
return http.StatusBadRequest
|
||
|
case codes.DeadlineExceeded:
|
||
|
return http.StatusGatewayTimeout
|
||
|
case codes.NotFound:
|
||
|
return http.StatusNotFound
|
||
|
case codes.AlreadyExists:
|
||
|
return http.StatusConflict
|
||
|
case codes.PermissionDenied:
|
||
|
return http.StatusForbidden
|
||
|
case codes.Unauthenticated:
|
||
|
return http.StatusUnauthorized
|
||
|
case codes.ResourceExhausted:
|
||
|
return http.StatusTooManyRequests
|
||
|
case codes.FailedPrecondition:
|
||
|
// Note, this deliberately doesn't translate to the similarly named '412 Precondition Failed' HTTP response status.
|
||
|
return http.StatusBadRequest
|
||
|
case codes.Aborted:
|
||
|
return http.StatusConflict
|
||
|
case codes.OutOfRange:
|
||
|
return http.StatusBadRequest
|
||
|
case codes.Unimplemented:
|
||
|
return http.StatusNotImplemented
|
||
|
case codes.Internal:
|
||
|
return http.StatusInternalServerError
|
||
|
case codes.Unavailable:
|
||
|
return http.StatusServiceUnavailable
|
||
|
case codes.DataLoss:
|
||
|
return http.StatusInternalServerError
|
||
|
}
|
||
|
|
||
|
grpclog.Infof("Unknown gRPC error code: %v", code)
|
||
|
return http.StatusInternalServerError
|
||
|
}
|
||
|
|
||
|
var (
|
||
|
// HTTPError replies to the request with an error.
|
||
|
//
|
||
|
// HTTPError is called:
|
||
|
// - From generated per-endpoint gateway handler code, when calling the backend results in an error.
|
||
|
// - From gateway runtime code, when forwarding the response message results in an error.
|
||
|
//
|
||
|
// The default value for HTTPError calls the custom error handler configured on the ServeMux via the
|
||
|
// WithProtoErrorHandler serve option if that option was used, calling GlobalHTTPErrorHandler otherwise.
|
||
|
//
|
||
|
// To customize the error handling of a particular ServeMux instance, use the WithProtoErrorHandler
|
||
|
// serve option.
|
||
|
//
|
||
|
// To customize the error format for all ServeMux instances not using the WithProtoErrorHandler serve
|
||
|
// option, set GlobalHTTPErrorHandler to a custom function.
|
||
|
//
|
||
|
// Setting this variable directly to customize error format is deprecated.
|
||
|
HTTPError = MuxOrGlobalHTTPError
|
||
|
|
||
|
// GlobalHTTPErrorHandler is the HTTPError handler for all ServeMux instances not using the
|
||
|
// WithProtoErrorHandler serve option.
|
||
|
//
|
||
|
// You can set a custom function to this variable to customize error format.
|
||
|
GlobalHTTPErrorHandler = DefaultHTTPError
|
||
|
|
||
|
// OtherErrorHandler handles gateway errors from parsing and routing client requests for all
|
||
|
// ServeMux instances not using the WithProtoErrorHandler serve option.
|
||
|
//
|
||
|
// It returns the following error codes: StatusMethodNotAllowed StatusNotFound StatusBadRequest
|
||
|
//
|
||
|
// To customize parsing and routing error handling of a particular ServeMux instance, use the
|
||
|
// WithProtoErrorHandler serve option.
|
||
|
//
|
||
|
// To customize parsing and routing error handling of all ServeMux instances not using the
|
||
|
// WithProtoErrorHandler serve option, set a custom function to this variable.
|
||
|
OtherErrorHandler = DefaultOtherErrorHandler
|
||
|
)
|
||
|
|
||
|
// MuxOrGlobalHTTPError uses the mux-configured error handler, falling back to GlobalErrorHandler.
|
||
|
func MuxOrGlobalHTTPError(ctx context.Context, mux *ServeMux, marshaler Marshaler, w http.ResponseWriter, r *http.Request, err error) {
|
||
|
if mux.protoErrorHandler != nil {
|
||
|
mux.protoErrorHandler(ctx, mux, marshaler, w, r, err)
|
||
|
} else {
|
||
|
GlobalHTTPErrorHandler(ctx, mux, marshaler, w, r, err)
|
||
|
}
|
||
|
}
|
||
|
|
||
|
// DefaultHTTPError is the default implementation of HTTPError.
|
||
|
// If "err" is an error from gRPC system, the function replies with the status code mapped by HTTPStatusFromCode.
|
||
|
// If otherwise, it replies with http.StatusInternalServerError.
|
||
|
//
|
||
|
// The response body returned by this function is a JSON object,
|
||
|
// which contains a member whose key is "error" and whose value is err.Error().
|
||
|
func DefaultHTTPError(ctx context.Context, mux *ServeMux, marshaler Marshaler, w http.ResponseWriter, r *http.Request, err error) {
|
||
|
const fallback = `{"error": "failed to marshal error message"}`
|
||
|
|
||
|
s, ok := status.FromError(err)
|
||
|
if !ok {
|
||
|
s = status.New(codes.Unknown, err.Error())
|
||
|
}
|
||
|
|
||
|
w.Header().Del("Trailer")
|
||
|
w.Header().Del("Transfer-Encoding")
|
||
|
|
||
|
contentType := marshaler.ContentType()
|
||
|
// Check marshaler on run time in order to keep backwards compatibility
|
||
|
// An interface param needs to be added to the ContentType() function on
|
||
|
// the Marshal interface to be able to remove this check
|
||
|
if typeMarshaler, ok := marshaler.(contentTypeMarshaler); ok {
|
||
|
pb := s.Proto()
|
||
|
contentType = typeMarshaler.ContentTypeFromMessage(pb)
|
||
|
}
|
||
|
w.Header().Set("Content-Type", contentType)
|
||
|
|
||
|
body := &internal.Error{
|
||
|
Error: s.Message(),
|
||
|
Message: s.Message(),
|
||
|
Code: int32(s.Code()),
|
||
|
Details: s.Proto().GetDetails(),
|
||
|
}
|
||
|
|
||
|
buf, merr := marshaler.Marshal(body)
|
||
|
if merr != nil {
|
||
|
grpclog.Infof("Failed to marshal error message %q: %v", body, merr)
|
||
|
w.WriteHeader(http.StatusInternalServerError)
|
||
|
if _, err := io.WriteString(w, fallback); err != nil {
|
||
|
grpclog.Infof("Failed to write response: %v", err)
|
||
|
}
|
||
|
return
|
||
|
}
|
||
|
|
||
|
md, ok := ServerMetadataFromContext(ctx)
|
||
|
if !ok {
|
||
|
grpclog.Infof("Failed to extract ServerMetadata from context")
|
||
|
}
|
||
|
|
||
|
handleForwardResponseServerMetadata(w, mux, md)
|
||
|
|
||
|
// RFC 7230 https://tools.ietf.org/html/rfc7230#section-4.1.2
|
||
|
// Unless the request includes a TE header field indicating "trailers"
|
||
|
// is acceptable, as described in Section 4.3, a server SHOULD NOT
|
||
|
// generate trailer fields that it believes are necessary for the user
|
||
|
// agent to receive.
|
||
|
var wantsTrailers bool
|
||
|
|
||
|
if te := r.Header.Get("TE"); strings.Contains(strings.ToLower(te), "trailers") {
|
||
|
wantsTrailers = true
|
||
|
handleForwardResponseTrailerHeader(w, md)
|
||
|
w.Header().Set("Transfer-Encoding", "chunked")
|
||
|
}
|
||
|
|
||
|
st := HTTPStatusFromCode(s.Code())
|
||
|
w.WriteHeader(st)
|
||
|
if _, err := w.Write(buf); err != nil {
|
||
|
grpclog.Infof("Failed to write response: %v", err)
|
||
|
}
|
||
|
|
||
|
if wantsTrailers {
|
||
|
handleForwardResponseTrailer(w, md)
|
||
|
}
|
||
|
}
|
||
|
|
||
|
// DefaultOtherErrorHandler is the default implementation of OtherErrorHandler.
|
||
|
// It simply writes a string representation of the given error into "w".
|
||
|
func DefaultOtherErrorHandler(w http.ResponseWriter, _ *http.Request, msg string, code int) {
|
||
|
http.Error(w, msg, code)
|
||
|
}
|