Customizing your gateway

Message serialization

Custom serializer

You might want to serialize request/response messages in MessagePack instead of JSON, for example:

  1. Write a custom implementation of Marshaler.

  2. Register your marshaler with WithMarshalerOption.

    e.g.

    1. var m your.MsgPackMarshaler
    2. mux := runtime.NewServeMux(
    3. runtime.WithMarshalerOption("application/x-msgpack", m),
    4. )

You can see the default implementation for JSON for reference.

Using proto names in JSON

The protocol buffer compiler generates camelCase JSON tags that are used by default. If you want to use the exact case used in the proto files, set UseProtoNames: true:

  1. mux := runtime.NewServeMux(
  2. runtime.WithMarshalerOption(runtime.MIMEWildcard, &runtime.JSONPb{
  3. MarshalOptions: protojson.MarshalOptions{
  4. UseProtoNames: true,
  5. },
  6. UnmarshalOptions: protojson.UnmarshalOptions{
  7. DiscardUnknown: true,
  8. },
  9. }),
  10. )

Pretty-print JSON responses when queried with ?pretty

You can have Elasticsearch-style ?pretty support in your gateway’s endpoints as follows:

  1. Wrap the ServeMux using a stdlib http.HandlerFunc that translates the provided query parameter into a custom Accept header.

  2. Register a pretty-printing marshaler for that MIME code.

For example:

  1. mux := runtime.NewServeMux(
  2. runtime.WithMarshalerOption("application/json+pretty", &runtime.JSONPb{
  3. MarshalOptions: protojson.MarshalOptions{
  4. Indent: " ",
  5. Multiline: true, // Optional, implied by presence of "Indent".
  6. },
  7. UnmarshalOptions: protojson.UnmarshalOptions{
  8. DiscardUnknown: true,
  9. },
  10. }),
  11. )
  12. prettier := func(h http.Handler) http.Handler {
  13. return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
  14. // checking Values as map[string][]string also catches ?pretty and ?pretty=
  15. // r.URL.Query().Get("pretty") would not.
  16. if _, ok := r.URL.Query()["pretty"]; ok {
  17. r.Header.Set("Accept", "application/json+pretty")
  18. }
  19. h.ServeHTTP(w, r)
  20. })
  21. }
  22. http.ListenAndServe(":8080", prettier(mux))

Now, either when passing the header Accept: application/json+pretty or appending ?pretty to your HTTP endpoints, the response will be pretty-printed.

Note that this will conflict with any methods having input messages with fields named pretty; also, this example code does not remove the query parameter pretty from further processing.

Customize unmarshaling per Content-Type

Having different unmarshaling options per Content-Type is as easy as configuring a custom marshaler:

  1. mux := runtime.NewServeMux(
  2. runtime.WithMarshalerOption("application/json+strict", &runtime.JSONPb{
  3. UnmarshalOptions: &protojson.UnmarshalOptions{
  4. DiscardUnknown: false, // explicit "false", &protojson.UnmarshalOptions{} would have the same effect
  5. },
  6. }),
  7. )

Mapping from HTTP request headers to gRPC client metadata

You might not like the default mapping rule and might want to pass through all the HTTP headers, for example:

  1. Write a HeaderMatcherFunc.

  2. Register the function with WithIncomingHeaderMatcher

    e.g.

    1. func CustomMatcher(key string) (string, bool) {
    2. switch key {
    3. case "X-Custom-Header1":
    4. return key, true
    5. case "X-Custom-Header2":
    6. return "custom-header2", true
    7. default:
    8. return key, false
    9. }
    10. }
    11. mux := runtime.NewServeMux(
    12. runtime.WithIncomingHeaderMatcher(CustomMatcher),
    13. )

To keep the the default mapping rule alongside with your own rules write:

  1. func CustomMatcher(key string) (string, bool) {
  2. switch key {
  3. case "X-User-Id":
  4. return key, true
  5. default:
  6. return runtime.DefaultHeaderMatcher(key)
  7. }
  8. }

It will work with both:

  1. $ curl --header "x-user-id: 100d9f38-2777-4ee2-ac3b-b3a108f81a30" ...

and

  1. $ curl --header "X-USER-ID: 100d9f38-2777-4ee2-ac3b-b3a108f81a30" ...

To access this header on gRPC server side use:

  1. userID := ""
  2. if md, ok := metadata.FromIncomingContext(ctx); ok {
  3. if uID, ok := md["x-user-id"]; ok {
  4. userID = strings.Join(uID, ",")
  5. }
  6. }

Mapping from gRPC server metadata to HTTP response headers

Use WithOutgoingHeaderMatcher. See gRPC metadata docs for more info on sending / receiving gRPC metadata, for example:

  1. if appendCustomHeader {
  2. grpc.SendHeader(ctx, metadata.New(map[string]string{
  3. "x-custom-header1": "value",
  4. }))
  5. }

Mutate response messages or set response headers

Set HTTP headers

You might want to return a subset of response fields as HTTP response headers; You might want to simply set an application-specific token in a header. Or you might want to mutate the response messages to be returned.

  1. Write a filter function.
  1. func myFilter(ctx context.Context, w http.ResponseWriter, resp proto.Message) error {
  2. t, ok := resp.(*externalpb.Tokenizer)
  3. if ok {
  4. w.Header().Set("X-My-Tracking-Token", t.Token)
  5. t.Token = ""
  6. }
  7. return nil
  8. }
  1. Register the filter with WithForwardResponseOption

e.g.

  1. mux := runtime.NewServeMux(
  2. runtime.WithForwardResponseOption(myFilter),
  3. )

Controlling HTTP response status codes

To have the most control over the HTTP response status codes, you can use custom metadata.

While handling the rpc, set the intended status code:

  1. _ = grpc.SetHeader(ctx, metadata.Pairs("x-http-code", "401"))

Now, before sending the HTTP response, we need to check for this metadata pair and explicitly set the status code for the response if found. To do so, create a function and hook it into the gRPC-Gateway as a Forward Response Option.

The function looks like this:

  1. func httpResponseModifier(ctx context.Context, w http.ResponseWriter, p proto.Message) error {
  2. md, ok := runtime.ServerMetadataFromContext(ctx)
  3. if !ok {
  4. return nil
  5. }
  6. // set http status code
  7. if vals := md.HeaderMD.Get("x-http-code"); len(vals) > 0 {
  8. code, err := strconv.Atoi(vals[0])
  9. if err != nil {
  10. return err
  11. }
  12. // delete the headers to not expose any grpc-metadata in http response
  13. delete(md.HeaderMD, "x-http-code")
  14. delete(w.Header(), "Grpc-Metadata-X-Http-Code")
  15. w.WriteHeader(code)
  16. }
  17. return nil
  18. }

And it gets hooked into the gRPC-Gateway with:

  1. gwMux := runtime.NewServeMux(
  2. runtime.WithForwardResponseOption(httpResponseModifier),
  3. )

Additional responses can be added to the Protocol Buffer definitions to match the new status codes:

  1. service Greeter {
  2. rpc SayHello (HelloRequest) returns (HelloReply) {
  3. option (google.api.http) = {
  4. post: "/v1/example/echo"
  5. body: "*"
  6. };
  7. option (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_operation) = {
  8. responses: {
  9. key: "201"
  10. value: {
  11. description: "A successful response."
  12. schema: {
  13. json_schema: {
  14. ref: ".mypackage.HelloReply"
  15. }
  16. }
  17. }
  18. }
  19. };
  20. }
  21. rpc SayGoodbye (GoodbyeRequest) returns (google.protobuf.Empty) {
  22. option (google.api.http) = {
  23. delete: "/v1/example/echo/{id}"
  24. };
  25. option (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_operation) = {
  26. responses: {
  27. key: "204"
  28. value: {
  29. description: "A successful response."
  30. schema: {}
  31. }
  32. }
  33. };
  34. }
  35. }

Error handler

To override error handling for a *runtime.ServeMux, use the runtime.WithErrorHandler option. This will configure all unary error responses to pass through this error handler.

Stream Error Handler

The error handler described in the previous section applies only to RPC methods that have a unary response.

When the method has a streaming response, gRPC-Gateway handles that by emitting a newline-separated stream of “chunks”. Each chunk is an envelope that can contain either a response message or an error. Only the last chunk will include an error, and only when the RPC handler ends abnormally (i.e. with an error code).

Because of the way the errors are included in the response body, the other error handler signature is insufficient. So for server streams, you must install a different error handler:

  1. mux := runtime.NewServeMux(
  2. runtime.WithStreamErrorHandler(handleStreamError),
  3. )

The signature of the handler is much more rigid because we need to know the structure of the error payload to properly encode the “chunk” schema into an OpenAPI spec.

So the function must return a *runtime.StreamError. The handler can choose to omit some fields and can filter/transform the original error, such as stripping stack traces from error messages.

Here’s an example custom handler:

  1. // handleStreamError overrides default behavior for computing an error
  2. // message for a server stream.
  3. //
  4. // It uses a default "502 Bad Gateway" HTTP code, only emits "safe"
  5. // messages and does not set the details field (so it will
  6. // be omitted from the resulting JSON object that is sent to client).
  7. func handleStreamError(ctx context.Context, err error) *status.Status {
  8. code := codes.Internal
  9. msg := "unexpected error"
  10. if s, ok := status.FromError(err); ok {
  11. code = s.Code()
  12. // default message, based on the gRPC status
  13. msg = s.Message()
  14. // see if error details include "safe" message to send
  15. // to external callers
  16. for _, msg := range s.Details() {
  17. if safe, ok := msg.(*SafeMessage); ok {
  18. msg = safe.Text
  19. break
  20. }
  21. }
  22. }
  23. return status.Errorf(code, msg)
  24. }

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.

Controlling path parameter unescaping

By default, gRPC-Gateway unescapes the entire URL path string attempting to route a request. This causes routing errors when the path parameter contains an illegal character such as /.

To replicate the behavior described in google.api.http, use runtime.WithUnescapingMode() to configure the unescaping behavior, as in the example below:

  1. mux := runtime.NewServeMux(
  2. runtime.WithUnescapingMode(runtime.UnescapingModeAllExceptReserved),
  3. )

For multi-segment parameters (e.g. {id=**}) RFC 6570 Reserved Expansion characters are left escaped and the gRPC API will need to unescape them.

To replicate the default V2 escaping behavior but also allow passing pct-encoded / characters, the ServeMux can be configured as in the example below:

  1. mux := runtime.NewServeMux(
  2. runtime.WithUnescapingMode(runtime.UnescapingModeAllCharacters),
  3. )

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. The 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.

Customizing Routing Errors

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:

  1. func handleRoutingError(ctx context.Context, mux *ServeMux, marshaler Marshaler, w http.ResponseWriter, r *http.Request, httpStatus int) {
  2. if httpStatus != http.StatusMethodNotAllowed {
  3. runtime.DefaultRoutingErrorHandler(ctx, mux, marshaler, writer, request, httpStatus)
  4. return
  5. }
  6. // Use HTTPStatusError to customize the DefaultHTTPErrorHandler status code
  7. err := &HTTPStatusError{
  8. HTTPStatus: httpStatus
  9. Err: status.Error(codes.Unimplemented, http.StatusText(httpStatus))
  10. }
  11. runtime.DefaultHTTPErrorHandler(ctx, mux, marshaler, w , r, err)
  12. }

To use this routing error handler, construct the mux as follows:

  1. mux := runtime.NewServeMux(
  2. runtime.WithRoutingErrorHandler(handleRoutingError),
  3. )