diff --git a/docs/_docs/customizingyourgateway.md b/docs/_docs/customizingyourgateway.md index 387a835c8a6..2be1b795ee8 100644 --- a/docs/_docs/customizingyourgateway.md +++ b/docs/_docs/customizingyourgateway.md @@ -303,3 +303,17 @@ If no custom handler is provided, the default stream error handler will include any gRPC error attributes (code, message, detail messages), if the error being reported includes them. If the error does not have these attributes, a gRPC code of `Unknown` (2) is reported. + +## Routing Error handler +To override the error behavior when `*runtime.ServeMux` was not +able to serve the request due to routing issues, use the `runtime.WithRoutingErrorHandler` option. + +This will configure all HTTP routing errors to pass through this error handler. +Default behavior is to map HTTP error codes to gRPC errors. + +HTTP statuses and their mappings to gRPC statuses: +* HTTP `404 Not Found` -> gRPC `5 NOT_FOUND` +* HTTP `405 Method Not Allowed` -> gRPC `12 UNIMPLEMENTED` +* HTTP `400 Bad Request` -> gRPC `3 INVALID_ARGUMENT` + +This method is not used outside of the initial routing. diff --git a/docs/_docs/v2-migration.md b/docs/_docs/v2-migration.md index e5e1ac960c2..aac1df3720b 100644 --- a/docs/_docs/v2-migration.md +++ b/docs/_docs/v2-migration.md @@ -148,3 +148,5 @@ services. `runtime.WithProtoErrorHandler` are all gone. Error handling is rewritten around the use of gRPCs Status types. If you wish to configure how the gateway handles errors, please use `runtime.WithErrorHandler` and `runtime.WithStreamErrorHandler`. +To handle routing errors (similar to the removed `runtime.OtherErrorHandler`) please use +`runtime.WithRoutingErrorHandler`. diff --git a/runtime/errors.go b/runtime/errors.go index 4c4619f6fba..9f207453395 100644 --- a/runtime/errors.go +++ b/runtime/errors.go @@ -17,6 +17,9 @@ type ErrorHandlerFunc func(context.Context, *ServeMux, Marshaler, http.ResponseW // StreamErrorHandlerFunc is the signature used to configure stream error handling. type StreamErrorHandlerFunc func(context.Context, error) *status.Status +// RoutingErrorHandlerFunc is the signature used to configure error handling for routing errors. +type RoutingErrorHandlerFunc func(context.Context, *ServeMux, Marshaler, http.ResponseWriter, *http.Request, int) + // 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 { @@ -129,3 +132,22 @@ func DefaultHTTPErrorHandler(ctx context.Context, mux *ServeMux, marshaler Marsh func DefaultStreamErrorHandler(_ context.Context, err error) *status.Status { return status.Convert(err) } + +// DefaultRoutingErrorHandler is our default handler for routing errors. +// By default http error codes mapped on the following error codes: +// NotFound -> grpc.NotFound +// StatusBadRequest -> grpc.InvalidArgument +// MethodNotAllowed -> grpc.Unimplemented +// Other -> grpc.Internal, method is not expecting to be called for anything else +func DefaultRoutingErrorHandler(ctx context.Context, mux *ServeMux, marshaler Marshaler, w http.ResponseWriter, r *http.Request, httpStatus int) { + sterr := status.Error(codes.Internal, "Unexpected routing error") + switch httpStatus { + case http.StatusBadRequest: + sterr = status.Error(codes.InvalidArgument, http.StatusText(httpStatus)) + case http.StatusMethodNotAllowed: + sterr = status.Error(codes.Unimplemented, http.StatusText(httpStatus)) + case http.StatusNotFound: + sterr = status.Error(codes.NotFound, http.StatusText(httpStatus)) + } + mux.errorHandler(ctx, mux, marshaler, w, r, sterr) +} diff --git a/runtime/mux.go b/runtime/mux.go index 919a18ba197..cad67407b5f 100644 --- a/runtime/mux.go +++ b/runtime/mux.go @@ -6,7 +6,7 @@ import ( "net/http" "net/textproto" "strings" - + "github.com/grpc-ecosystem/grpc-gateway/v2/internal/httprule" "google.golang.org/grpc/codes" "google.golang.org/grpc/metadata" @@ -29,6 +29,7 @@ type ServeMux struct { metadataAnnotators []func(context.Context, *http.Request) metadata.MD errorHandler ErrorHandlerFunc streamErrorHandler StreamErrorHandlerFunc + routingErrorHandler RoutingErrorHandlerFunc disablePathLengthFallback bool } @@ -126,6 +127,16 @@ func WithStreamErrorHandler(fn StreamErrorHandlerFunc) ServeMuxOption { } } +// WithRoutingErrorHandler returns a ServeMuxOption for configuring a custom error handler to handle http routing errors. +// +// Method called for errors which can happen before gRPC route selected or executed. +// The following error codes: StatusMethodNotAllowed StatusNotFound StatusBadRequest +func WithRoutingErrorHandler(fn RoutingErrorHandlerFunc) ServeMuxOption { + return func(serveMux *ServeMux) { + serveMux.routingErrorHandler = fn + } +} + // WithDisablePathLengthFallback returns a ServeMuxOption for disable path length fallback. func WithDisablePathLengthFallback() ServeMuxOption { return func(serveMux *ServeMux) { @@ -141,6 +152,7 @@ func NewServeMux(opts ...ServeMuxOption) *ServeMux { marshalers: makeMarshalerMIMERegistry(), errorHandler: DefaultHTTPErrorHandler, streamErrorHandler: DefaultStreamErrorHandler, + routingErrorHandler: DefaultRoutingErrorHandler, } for _, opt := range opts { @@ -188,8 +200,7 @@ func (s *ServeMux) ServeHTTP(w http.ResponseWriter, r *http.Request) { path := r.URL.Path if !strings.HasPrefix(path, "/") { _, outboundMarshaler := MarshalerForRequest(s, r) - sterr := status.Error(codes.InvalidArgument, http.StatusText(http.StatusBadRequest)) - s.errorHandler(ctx, s, outboundMarshaler, w, r, sterr) + s.routingErrorHandler(ctx, s, outboundMarshaler, w, r, http.StatusBadRequest) return } @@ -199,8 +210,7 @@ func (s *ServeMux) ServeHTTP(w http.ResponseWriter, r *http.Request) { idx := strings.LastIndex(components[l-1], ":") if idx == 0 { _, outboundMarshaler := MarshalerForRequest(s, r) - sterr := status.Error(codes.NotFound, http.StatusText(http.StatusNotFound)) - s.errorHandler(ctx, s, outboundMarshaler, w, r, sterr) + s.routingErrorHandler(ctx, s, outboundMarshaler, w, r, http.StatusNotFound) return } if idx > 0 { @@ -249,16 +259,13 @@ func (s *ServeMux) ServeHTTP(w http.ResponseWriter, r *http.Request) { return } _, outboundMarshaler := MarshalerForRequest(s, r) - // codes.Unimplemented is the closes we have to MethodNotAllowed - sterr := status.Error(codes.Unimplemented, http.StatusText(http.StatusNotImplemented)) - s.errorHandler(ctx, s, outboundMarshaler, w, r, sterr) + s.routingErrorHandler(ctx, s, outboundMarshaler, w, r, http.StatusMethodNotAllowed) return } } _, outboundMarshaler := MarshalerForRequest(s, r) - sterr := status.Error(codes.NotFound, http.StatusText(http.StatusNotFound)) - s.errorHandler(ctx, s, outboundMarshaler, w, r, sterr) + s.routingErrorHandler(ctx, s, outboundMarshaler, w, r, http.StatusNotFound) } // GetForwardResponseOptions returns the ForwardResponseOptions associated with this ServeMux. diff --git a/runtime/mux_test.go b/runtime/mux_test.go index 6669d57f417..1b0ff1b0a86 100644 --- a/runtime/mux_test.go +++ b/runtime/mux_test.go @@ -293,6 +293,21 @@ func TestMuxServeHTTP(t *testing.T) { respStatus: http.StatusOK, respContent: "POST /foo/{id=*}:verb", }, + { + patterns: []stubPattern{ + { + method: "GET", + ops: []int{int(utilities.OpLitPush), 0}, + pool: []string{"foo"}, + }, + }, + reqMethod: "POST", + reqPath: "foo", + headers: map[string]string{ + "Content-Type": "application/json", + }, + respStatus: http.StatusBadRequest, + }, } { t.Run(strconv.Itoa(i), func(t *testing.T) { var opts []runtime.ServeMuxOption @@ -375,8 +390,6 @@ func TestDefaultHeaderMatcher(t *testing.T) { } } - - var defaultRouteMatcherTests = []struct { name string method string @@ -425,4 +438,4 @@ func TestServeMux_HandlePath(t *testing.T) { }) } -} \ No newline at end of file +}