Skip to content

Commit 995534d

Browse files
authored
Handle custom status code mappings for routing errors (#2101)
* Handle custom status code mappings for routing errors Not all gRPC error codes and http status codes can have a one to one mapping. Http can have both a path and a method component, while gRPC will just have a function that is specific to the combination of method and path when translating between the two. That means that for gRPC a missing function simply means "unimplemented", while for the equivalent REST API it is common to distinguish an unhandled method request on a known resource as "Method Not Allowed" with the response code 405. This may not be the only place where expectations of response codes needed may not align with what is provided by the current mapping chain http.Status* -> gRPC code.Codes -> http.Status*, where some information may be lost in the double conversion. As the error handler accepts an 'error' interface, it should be possible to provide a custom error structure that can contain the http Status code to wrap the original error to allow for the expected response code to be provided without impacting the current function signatures. * update with goimports * Restore DefaultRoutingErrorHandler and tests, and update doc strings Remove the customization from the DefaultRoutingErrorHandler as this is what should be done by consumers, consequently remove the modifications to tests affected. Update doc strings as requested. * Rename StatusHTTP to HTTPStatus * Update names and docstrings * Update customizing your gateway docs * rework example * remove newline
1 parent 43cbfd2 commit 995534d

File tree

3 files changed

+63
-0
lines changed

3 files changed

+63
-0
lines changed

docs/docs/mapping/customizing_your_gateway.md

Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -358,3 +358,31 @@ HTTP statuses and their mappings to gRPC statuses:
358358
- HTTP `400 Bad Request` -> gRPC `3 INVALID_ARGUMENT`
359359

360360
This method is not used outside of the initial routing.
361+
362+
### Customizing Routing Errors
363+
364+
If you want to retain HTTP `405 Method Not Allowed` instead of allowing it to be converted to the equivalent of the gRPC `12 UNIMPLEMENTED`, which is HTTP `501 Not Implmented` you can use the following example:
365+
366+
```go
367+
func handleRoutingError(ctx context.Context, mux *ServeMux, marshaler Marshaler, w http.ResponseWriter, r *http.Request, httpStatus int) {
368+
if httpStatus != http.StatusMethodNotAllowed {
369+
runtime.DefaultRoutingErrorHandler(ctx, mux, marshaler, writer, request, httpStatus)
370+
return
371+
}
372+
373+
// Use HTTPStatusError to customize the DefaultHTTPErrorHandler status code
374+
err := &HTTPStatusError{
375+
HTTPStatus: httpStatus
376+
Err: status.Error(codes.Unimplemented, http.StatusText(httpStatus))
377+
}
378+
379+
runtime.DefaultHTTPErrorHandler(ctx, mux, marshaler, w , r, err)
380+
}
381+
```
382+
383+
To use this routing error handler, construct the mux as follows:
384+
```go
385+
mux := runtime.NewServeMux(
386+
runtime.WithRoutingErrorHandler(handleRoutingError),
387+
)
388+
```

runtime/errors.go

Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@ package runtime
22

33
import (
44
"context"
5+
"errors"
56
"io"
67
"net/http"
78
"strings"
@@ -20,6 +21,17 @@ type StreamErrorHandlerFunc func(context.Context, error) *status.Status
2021
// RoutingErrorHandlerFunc is the signature used to configure error handling for routing errors.
2122
type RoutingErrorHandlerFunc func(context.Context, *ServeMux, Marshaler, http.ResponseWriter, *http.Request, int)
2223

24+
// HTTPStatusError is the error to use when needing to provide a different HTTP status code for an error
25+
// passed to the DefaultRoutingErrorHandler.
26+
type HTTPStatusError struct {
27+
HTTPStatus int
28+
Err error
29+
}
30+
31+
func (e *HTTPStatusError) Error() string {
32+
return e.Err.Error()
33+
}
34+
2335
// HTTPStatusFromCode converts a gRPC error code into the corresponding HTTP response status.
2436
// See: https://github.com/googleapis/googleapis/blob/master/google/rpc/code.proto
2537
func HTTPStatusFromCode(code codes.Code) int {
@@ -72,13 +84,22 @@ func HTTPError(ctx context.Context, mux *ServeMux, marshaler Marshaler, w http.R
7284

7385
// DefaultHTTPErrorHandler is the default error handler.
7486
// If "err" is a gRPC Status, the function replies with the status code mapped by HTTPStatusFromCode.
87+
// If "err" is a HTTPStatusError, the function replies with the status code provide by that struct. This is
88+
// intended to allow passing through of specific statuses via the function set via WithRoutingErrorHandler
89+
// for the ServeMux constructor to handle edge cases which the standard mappings in HTTPStatusFromCode
90+
// are insufficient for.
7591
// If otherwise, it replies with http.StatusInternalServerError.
7692
//
7793
// The response body written by this function is a Status message marshaled by the Marshaler.
7894
func DefaultHTTPErrorHandler(ctx context.Context, mux *ServeMux, marshaler Marshaler, w http.ResponseWriter, r *http.Request, err error) {
7995
// return Internal when Marshal failed
8096
const fallback = `{"code": 13, "message": "failed to marshal error message"}`
8197

98+
var customStatus *HTTPStatusError
99+
if errors.As(err, &customStatus) {
100+
err = customStatus.Err
101+
}
102+
82103
s := status.Convert(err)
83104
pb := s.Proto()
84105

@@ -119,6 +140,10 @@ func DefaultHTTPErrorHandler(ctx context.Context, mux *ServeMux, marshaler Marsh
119140
}
120141

121142
st := HTTPStatusFromCode(s.Code())
143+
if customStatus != nil {
144+
st = customStatus.HTTPStatus
145+
}
146+
122147
w.WriteHeader(st)
123148
if _, err := w.Write(buf); err != nil {
124149
grpclog.Infof("Failed to write response: %v", err)

runtime/errors_test.go

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -60,6 +60,16 @@ func TestDefaultHTTPError(t *testing.T) {
6060
contentType: "Custom-Content-Type",
6161
msg: "example error",
6262
},
63+
{
64+
err: &runtime.HTTPStatusError{
65+
HTTPStatus: http.StatusMethodNotAllowed,
66+
Err: status.Error(codes.Unimplemented, http.StatusText(http.StatusMethodNotAllowed)),
67+
},
68+
status: http.StatusMethodNotAllowed,
69+
marshaler: &runtime.JSONPb{},
70+
contentType: "application/json",
71+
msg: "Method Not Allowed",
72+
},
6373
} {
6474
t.Run(strconv.Itoa(i), func(t *testing.T) {
6575
w := httptest.NewRecorder()

0 commit comments

Comments
 (0)