khenaidoo | 59ce9dd | 2019-11-11 13:05:32 -0500 | [diff] [blame] | 1 | package runtime |
| 2 | |
| 3 | import ( |
| 4 | "context" |
| 5 | "io" |
| 6 | "net/http" |
khenaidoo | 2672188 | 2021-08-11 17:42:52 -0400 | [diff] [blame] | 7 | "strings" |
khenaidoo | 59ce9dd | 2019-11-11 13:05:32 -0500 | [diff] [blame] | 8 | |
khenaidoo | 2672188 | 2021-08-11 17:42:52 -0400 | [diff] [blame] | 9 | "github.com/grpc-ecosystem/grpc-gateway/internal" |
khenaidoo | 59ce9dd | 2019-11-11 13:05:32 -0500 | [diff] [blame] | 10 | "google.golang.org/grpc/codes" |
| 11 | "google.golang.org/grpc/grpclog" |
| 12 | "google.golang.org/grpc/status" |
| 13 | ) |
| 14 | |
| 15 | // HTTPStatusFromCode converts a gRPC error code into the corresponding HTTP response status. |
| 16 | // See: https://github.com/googleapis/googleapis/blob/master/google/rpc/code.proto |
| 17 | func HTTPStatusFromCode(code codes.Code) int { |
| 18 | switch code { |
| 19 | case codes.OK: |
| 20 | return http.StatusOK |
| 21 | case codes.Canceled: |
| 22 | return http.StatusRequestTimeout |
| 23 | case codes.Unknown: |
| 24 | return http.StatusInternalServerError |
| 25 | case codes.InvalidArgument: |
| 26 | return http.StatusBadRequest |
| 27 | case codes.DeadlineExceeded: |
| 28 | return http.StatusGatewayTimeout |
| 29 | case codes.NotFound: |
| 30 | return http.StatusNotFound |
| 31 | case codes.AlreadyExists: |
| 32 | return http.StatusConflict |
| 33 | case codes.PermissionDenied: |
| 34 | return http.StatusForbidden |
| 35 | case codes.Unauthenticated: |
| 36 | return http.StatusUnauthorized |
| 37 | case codes.ResourceExhausted: |
| 38 | return http.StatusTooManyRequests |
| 39 | case codes.FailedPrecondition: |
| 40 | // Note, this deliberately doesn't translate to the similarly named '412 Precondition Failed' HTTP response status. |
| 41 | return http.StatusBadRequest |
| 42 | case codes.Aborted: |
| 43 | return http.StatusConflict |
| 44 | case codes.OutOfRange: |
| 45 | return http.StatusBadRequest |
| 46 | case codes.Unimplemented: |
| 47 | return http.StatusNotImplemented |
| 48 | case codes.Internal: |
| 49 | return http.StatusInternalServerError |
| 50 | case codes.Unavailable: |
| 51 | return http.StatusServiceUnavailable |
| 52 | case codes.DataLoss: |
| 53 | return http.StatusInternalServerError |
| 54 | } |
| 55 | |
| 56 | grpclog.Infof("Unknown gRPC error code: %v", code) |
| 57 | return http.StatusInternalServerError |
| 58 | } |
| 59 | |
| 60 | var ( |
khenaidoo | 2672188 | 2021-08-11 17:42:52 -0400 | [diff] [blame] | 61 | // HTTPError replies to the request with an error. |
| 62 | // |
| 63 | // HTTPError is called: |
| 64 | // - From generated per-endpoint gateway handler code, when calling the backend results in an error. |
| 65 | // - From gateway runtime code, when forwarding the response message results in an error. |
| 66 | // |
| 67 | // The default value for HTTPError calls the custom error handler configured on the ServeMux via the |
| 68 | // WithProtoErrorHandler serve option if that option was used, calling GlobalHTTPErrorHandler otherwise. |
| 69 | // |
| 70 | // To customize the error handling of a particular ServeMux instance, use the WithProtoErrorHandler |
| 71 | // serve option. |
| 72 | // |
| 73 | // To customize the error format for all ServeMux instances not using the WithProtoErrorHandler serve |
| 74 | // option, set GlobalHTTPErrorHandler to a custom function. |
| 75 | // |
| 76 | // Setting this variable directly to customize error format is deprecated. |
| 77 | HTTPError = MuxOrGlobalHTTPError |
| 78 | |
| 79 | // GlobalHTTPErrorHandler is the HTTPError handler for all ServeMux instances not using the |
| 80 | // WithProtoErrorHandler serve option. |
| 81 | // |
khenaidoo | 59ce9dd | 2019-11-11 13:05:32 -0500 | [diff] [blame] | 82 | // You can set a custom function to this variable to customize error format. |
khenaidoo | 2672188 | 2021-08-11 17:42:52 -0400 | [diff] [blame] | 83 | GlobalHTTPErrorHandler = DefaultHTTPError |
| 84 | |
| 85 | // OtherErrorHandler handles gateway errors from parsing and routing client requests for all |
| 86 | // ServeMux instances not using the WithProtoErrorHandler serve option. |
| 87 | // |
| 88 | // It returns the following error codes: StatusMethodNotAllowed StatusNotFound StatusBadRequest |
| 89 | // |
| 90 | // To customize parsing and routing error handling of a particular ServeMux instance, use the |
| 91 | // WithProtoErrorHandler serve option. |
| 92 | // |
| 93 | // To customize parsing and routing error handling of all ServeMux instances not using the |
| 94 | // WithProtoErrorHandler serve option, set a custom function to this variable. |
khenaidoo | 59ce9dd | 2019-11-11 13:05:32 -0500 | [diff] [blame] | 95 | OtherErrorHandler = DefaultOtherErrorHandler |
| 96 | ) |
| 97 | |
khenaidoo | 2672188 | 2021-08-11 17:42:52 -0400 | [diff] [blame] | 98 | // MuxOrGlobalHTTPError uses the mux-configured error handler, falling back to GlobalErrorHandler. |
| 99 | func MuxOrGlobalHTTPError(ctx context.Context, mux *ServeMux, marshaler Marshaler, w http.ResponseWriter, r *http.Request, err error) { |
| 100 | if mux.protoErrorHandler != nil { |
| 101 | mux.protoErrorHandler(ctx, mux, marshaler, w, r, err) |
| 102 | } else { |
| 103 | GlobalHTTPErrorHandler(ctx, mux, marshaler, w, r, err) |
| 104 | } |
khenaidoo | 59ce9dd | 2019-11-11 13:05:32 -0500 | [diff] [blame] | 105 | } |
| 106 | |
khenaidoo | 59ce9dd | 2019-11-11 13:05:32 -0500 | [diff] [blame] | 107 | // DefaultHTTPError is the default implementation of HTTPError. |
| 108 | // If "err" is an error from gRPC system, the function replies with the status code mapped by HTTPStatusFromCode. |
| 109 | // If otherwise, it replies with http.StatusInternalServerError. |
| 110 | // |
| 111 | // The response body returned by this function is a JSON object, |
| 112 | // which contains a member whose key is "error" and whose value is err.Error(). |
khenaidoo | 2672188 | 2021-08-11 17:42:52 -0400 | [diff] [blame] | 113 | func DefaultHTTPError(ctx context.Context, mux *ServeMux, marshaler Marshaler, w http.ResponseWriter, r *http.Request, err error) { |
khenaidoo | 59ce9dd | 2019-11-11 13:05:32 -0500 | [diff] [blame] | 114 | const fallback = `{"error": "failed to marshal error message"}` |
| 115 | |
| 116 | s, ok := status.FromError(err) |
| 117 | if !ok { |
| 118 | s = status.New(codes.Unknown, err.Error()) |
| 119 | } |
| 120 | |
| 121 | w.Header().Del("Trailer") |
khenaidoo | 2672188 | 2021-08-11 17:42:52 -0400 | [diff] [blame] | 122 | w.Header().Del("Transfer-Encoding") |
khenaidoo | 59ce9dd | 2019-11-11 13:05:32 -0500 | [diff] [blame] | 123 | |
| 124 | contentType := marshaler.ContentType() |
khenaidoo | 2672188 | 2021-08-11 17:42:52 -0400 | [diff] [blame] | 125 | // Check marshaler on run time in order to keep backwards compatibility |
khenaidoo | 59ce9dd | 2019-11-11 13:05:32 -0500 | [diff] [blame] | 126 | // An interface param needs to be added to the ContentType() function on |
| 127 | // the Marshal interface to be able to remove this check |
khenaidoo | 2672188 | 2021-08-11 17:42:52 -0400 | [diff] [blame] | 128 | if typeMarshaler, ok := marshaler.(contentTypeMarshaler); ok { |
khenaidoo | 59ce9dd | 2019-11-11 13:05:32 -0500 | [diff] [blame] | 129 | pb := s.Proto() |
khenaidoo | 2672188 | 2021-08-11 17:42:52 -0400 | [diff] [blame] | 130 | contentType = typeMarshaler.ContentTypeFromMessage(pb) |
khenaidoo | 59ce9dd | 2019-11-11 13:05:32 -0500 | [diff] [blame] | 131 | } |
| 132 | w.Header().Set("Content-Type", contentType) |
| 133 | |
khenaidoo | 2672188 | 2021-08-11 17:42:52 -0400 | [diff] [blame] | 134 | body := &internal.Error{ |
khenaidoo | 59ce9dd | 2019-11-11 13:05:32 -0500 | [diff] [blame] | 135 | Error: s.Message(), |
| 136 | Message: s.Message(), |
| 137 | Code: int32(s.Code()), |
| 138 | Details: s.Proto().GetDetails(), |
| 139 | } |
| 140 | |
| 141 | buf, merr := marshaler.Marshal(body) |
| 142 | if merr != nil { |
| 143 | grpclog.Infof("Failed to marshal error message %q: %v", body, merr) |
| 144 | w.WriteHeader(http.StatusInternalServerError) |
| 145 | if _, err := io.WriteString(w, fallback); err != nil { |
| 146 | grpclog.Infof("Failed to write response: %v", err) |
| 147 | } |
| 148 | return |
| 149 | } |
| 150 | |
| 151 | md, ok := ServerMetadataFromContext(ctx) |
| 152 | if !ok { |
| 153 | grpclog.Infof("Failed to extract ServerMetadata from context") |
| 154 | } |
| 155 | |
| 156 | handleForwardResponseServerMetadata(w, mux, md) |
khenaidoo | 2672188 | 2021-08-11 17:42:52 -0400 | [diff] [blame] | 157 | |
| 158 | // RFC 7230 https://tools.ietf.org/html/rfc7230#section-4.1.2 |
| 159 | // Unless the request includes a TE header field indicating "trailers" |
| 160 | // is acceptable, as described in Section 4.3, a server SHOULD NOT |
| 161 | // generate trailer fields that it believes are necessary for the user |
| 162 | // agent to receive. |
| 163 | var wantsTrailers bool |
| 164 | |
| 165 | if te := r.Header.Get("TE"); strings.Contains(strings.ToLower(te), "trailers") { |
| 166 | wantsTrailers = true |
| 167 | handleForwardResponseTrailerHeader(w, md) |
| 168 | w.Header().Set("Transfer-Encoding", "chunked") |
| 169 | } |
| 170 | |
khenaidoo | 59ce9dd | 2019-11-11 13:05:32 -0500 | [diff] [blame] | 171 | st := HTTPStatusFromCode(s.Code()) |
| 172 | w.WriteHeader(st) |
| 173 | if _, err := w.Write(buf); err != nil { |
| 174 | grpclog.Infof("Failed to write response: %v", err) |
| 175 | } |
| 176 | |
khenaidoo | 2672188 | 2021-08-11 17:42:52 -0400 | [diff] [blame] | 177 | if wantsTrailers { |
| 178 | handleForwardResponseTrailer(w, md) |
| 179 | } |
khenaidoo | 59ce9dd | 2019-11-11 13:05:32 -0500 | [diff] [blame] | 180 | } |
| 181 | |
| 182 | // DefaultOtherErrorHandler is the default implementation of OtherErrorHandler. |
| 183 | // It simply writes a string representation of the given error into "w". |
| 184 | func DefaultOtherErrorHandler(w http.ResponseWriter, _ *http.Request, msg string, code int) { |
| 185 | http.Error(w, msg, code) |
| 186 | } |