🐛 Bug Report
Not an actual bug, more a PSA for others, with similar issues, to stumble upon.
We had a component consisting of a runtime.WithMetadata annotator which added multiple fields to the gRPC context and a UnaryServerInterceptor that reads the previously added metadata fields from the gRPC context.
One of the added fields was the original HTTP request body.
The grpc-gateway usually creates a NewDecoder from the marshaler of the specific request and calls Decode on the request body.
In our setup we've used the default marshaler, so runtime.HTTPBodyMarshaler which is wrapping runtime.JSONPb.
Now to our actual problem:
The UnaryServerInterceptor retrieves the original HTTP request body from the gRPC context and calls json.Unmarshal on the raw bytes.
This is not a problem for normal JSON bodies that are entirely valid.
But for requests where, for whatever reason, additional bytes are present after the initial JSON object, it will result in grpc-gateway being able to properly Decode the body, passing it to the business logic, and json.Unmarshal in the UnaryServerInterceptor later failing to unmarshal.
Such an invalid body might look like this:
{"a": "b"}x or {"a":"b"}{"x":"y"}
This happens because of the difference between runtime.JSONPb, which uses json.Decoder and json.Unmarshal.
The json.Decoder decodes the input []byte JSON object by JSON object, while json.Unmarshal tries to decode the entire input []byte.
The solution:
- If you control the code which uses
json.Unmarshal, is quite easy: Just use the json.Decoder & Decode instead.
- If you are unable to change the code and/or instead want to ensure that the original HTTP request body only contains the valid JSON object and nothing else: Configure grpc-gateway to use a custom marshaler (
runtime.WithMarshalerOption) that wraps runtime.jsonPb.
An implementation of such a custom marshaler wrapper might look like this:
Custom Marshaler
package jsonpb
import (
"bytes"
"encoding/json"
"errors"
"io"
"github.com/grpc-ecosystem/grpc-gateway/v2/runtime"
)
type JSONPb struct {
runtime.JSONPb
}
func (j *JSONPb) NewDecoder(r io.Reader) runtime.Decoder {
d := json.NewDecoder(r)
return DecoderWrapper{
original: runtime.DecoderWrapper{
Decoder: d,
UnmarshalOptions: j.UnmarshalOptions,
},
}
}
type DecoderWrapper struct {
original runtime.DecoderWrapper
}
func (d DecoderWrapper) Decode(v any) error {
if err := d.original.Decode(v); err != nil {
return err
}
_, err := d.original.Token()
if err != io.EOF {
return errors.New("request body is not a valid JSON object: body contains additional bytes after the JSON object")
}
return nil
}
func (j *JSONPb) Unmarshal(data []byte, v any) error {
return j.NewDecoder(bytes.NewReader(data)).Decode(v)
}
Configure grpc-gateway like this:
runtime.WithMarshalerOption(runtime.MIMEWildcard, &runtime.HTTPBodyMarshaler{
Marshaler: &jsonpb.JSONPb{
JSONPb: runtime.JSONPb{
MarshalOptions: protojson.MarshalOptions{
EmitUnpopulated: true,
},
UnmarshalOptions: protojson.UnmarshalOptions{
DiscardUnknown: true,
},
},
},
}),
I hope this is useful to someone 🙂
(Feel free to close this issue)
🐛 Bug Report
Not an actual bug, more a PSA for others, with similar issues, to stumble upon.
We had a component consisting of a
runtime.WithMetadataannotator which added multiple fields to the gRPC context and aUnaryServerInterceptorthat reads the previously added metadata fields from the gRPC context.One of the added fields was the original HTTP request body.
The grpc-gateway usually creates a
NewDecoderfrom the marshaler of the specific request and callsDecodeon the request body.In our setup we've used the default marshaler, so
runtime.HTTPBodyMarshalerwhich is wrappingruntime.JSONPb.Now to our actual problem:
The
UnaryServerInterceptorretrieves the original HTTP request body from the gRPC context and callsjson.Unmarshalon the raw bytes.This is not a problem for normal JSON bodies that are entirely valid.
But for requests where, for whatever reason, additional bytes are present after the initial JSON object, it will result in grpc-gateway being able to properly
Decodethe body, passing it to the business logic, andjson.Unmarshalin theUnaryServerInterceptorlater failing to unmarshal.Such an invalid body might look like this:
{"a": "b"}xor{"a":"b"}{"x":"y"}This happens because of the difference between
runtime.JSONPb, which usesjson.Decoderandjson.Unmarshal.The
json.Decoderdecodes the input[]byteJSON object by JSON object, whilejson.Unmarshaltries to decode the entire input[]byte.The solution:
json.Unmarshal, is quite easy: Just use thejson.Decoder&Decodeinstead.runtime.WithMarshalerOption) that wrapsruntime.jsonPb.An implementation of such a custom marshaler wrapper might look like this:
Custom Marshaler
Configure grpc-gateway like this:
I hope this is useful to someone 🙂
(Feel free to close this issue)