Skip to content

Commit

Permalink
Add HTTP routing errors handler (#1517)
Browse files Browse the repository at this point in the history
Co-authored-by: Johan Brandhorst <[email protected]>
  • Loading branch information
octo47 and johanbrandhorst authored Jul 13, 2020
1 parent 8393691 commit 050e889
Show file tree
Hide file tree
Showing 5 changed files with 71 additions and 13 deletions.
14 changes: 14 additions & 0 deletions docs/_docs/customizingyourgateway.md
Original file line number Diff line number Diff line change
Expand Up @@ -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.
2 changes: 2 additions & 0 deletions docs/_docs/v2-migration.md
Original file line number Diff line number Diff line change
Expand Up @@ -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`.
22 changes: 22 additions & 0 deletions runtime/errors.go
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Expand Down Expand Up @@ -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)
}
27 changes: 17 additions & 10 deletions runtime/mux.go
Original file line number Diff line number Diff line change
Expand Up @@ -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"
Expand All @@ -29,6 +29,7 @@ type ServeMux struct {
metadataAnnotators []func(context.Context, *http.Request) metadata.MD
errorHandler ErrorHandlerFunc
streamErrorHandler StreamErrorHandlerFunc
routingErrorHandler RoutingErrorHandlerFunc
disablePathLengthFallback bool
}

Expand Down Expand Up @@ -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) {
Expand All @@ -141,6 +152,7 @@ func NewServeMux(opts ...ServeMuxOption) *ServeMux {
marshalers: makeMarshalerMIMERegistry(),
errorHandler: DefaultHTTPErrorHandler,
streamErrorHandler: DefaultStreamErrorHandler,
routingErrorHandler: DefaultRoutingErrorHandler,
}

for _, opt := range opts {
Expand Down Expand Up @@ -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
}

Expand All @@ -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 {
Expand Down Expand Up @@ -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.
Expand Down
19 changes: 16 additions & 3 deletions runtime/mux_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -375,8 +390,6 @@ func TestDefaultHeaderMatcher(t *testing.T) {
}
}



var defaultRouteMatcherTests = []struct {
name string
method string
Expand Down Expand Up @@ -425,4 +438,4 @@ func TestServeMux_HandlePath(t *testing.T) {
})
}

}
}

0 comments on commit 050e889

Please sign in to comment.