mirror of https://github.com/dapr/go-sdk.git
Decode CloudEvent Data consistently between HTTP and gRPC (#230)
* Decode CloudEvent Data consistently between HTTP and gRPC Signed-off-by: Phil Kedy <phil.kedy@gmail.com> * linter issue Signed-off-by: Phil Kedy <phil.kedy@gmail.com> * Added parsing media type Signed-off-by: Phil Kedy <phil.kedy@gmail.com> * Adding extension json media type handling Signed-off-by: Phil Kedy <phil.kedy@gmail.com> * add TTL to actor timer/reminder requests (#225) Signed-off-by: Phil Kedy <phil.kedy@gmail.com> * Adding more tests Signed-off-by: Phil Kedy <phil.kedy@gmail.com> * Tweak Signed-off-by: Phil Kedy <phil.kedy@gmail.com> Co-authored-by: Dmitry Shmulevich <dmitry.shmulevich@gmail.com>
This commit is contained in:
parent
15fc672e08
commit
8a7a61b443
|
@ -1,5 +1,9 @@
|
|||
package common
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
)
|
||||
|
||||
// TopicEvent is the content of the inbound topic message.
|
||||
type TopicEvent struct {
|
||||
// ID identifies the event.
|
||||
|
@ -15,8 +19,12 @@ type TopicEvent struct {
|
|||
// The content of the event.
|
||||
// Note, this is why the gRPC and HTTP implementations need separate structs for cloud events.
|
||||
Data interface{} `json:"data"`
|
||||
// The content of the event represented as raw bytes.
|
||||
// This can be deserialized into a Go struct using `Struct`.
|
||||
RawData []byte `json:"-"`
|
||||
// The base64 encoding content of the event.
|
||||
// Note, this is processing rawPayload and binary content types .
|
||||
// Note, this is processing rawPayload and binary content types.
|
||||
// This field is deprecated and will be removed in the future.
|
||||
DataBase64 string `json:"data_base64,omitempty"`
|
||||
// Cloud event subject
|
||||
Subject string `json:"subject"`
|
||||
|
@ -26,6 +34,12 @@ type TopicEvent struct {
|
|||
PubsubName string `json:"pubsubname"`
|
||||
}
|
||||
|
||||
func (e *TopicEvent) Struct(target interface{}) error {
|
||||
// TODO: Enhance to inspect DataContentType for the best
|
||||
// deserialization method.
|
||||
return json.Unmarshal(e.RawData, target)
|
||||
}
|
||||
|
||||
// InvocationEvent represents the input and output of binding invocation.
|
||||
type InvocationEvent struct {
|
||||
// Data is the payload that the input bindings sent.
|
||||
|
|
|
@ -2,7 +2,10 @@ package grpc
|
|||
|
||||
import (
|
||||
"context"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"mime"
|
||||
"strings"
|
||||
|
||||
"github.com/golang/protobuf/ptypes/empty"
|
||||
"github.com/pkg/errors"
|
||||
|
@ -62,13 +65,38 @@ func (s *Server) OnTopicEvent(ctx context.Context, in *pb.TopicEventRequest) (*p
|
|||
}
|
||||
key := fmt.Sprintf("%s-%s", in.PubsubName, in.Topic)
|
||||
if h, ok := s.topicSubscriptions[key]; ok {
|
||||
data := interface{}(in.Data)
|
||||
if len(in.Data) > 0 {
|
||||
mediaType, _, err := mime.ParseMediaType(in.DataContentType)
|
||||
if err == nil {
|
||||
var v interface{}
|
||||
switch mediaType {
|
||||
case "application/json":
|
||||
if err := json.Unmarshal(in.Data, &v); err == nil {
|
||||
data = v
|
||||
}
|
||||
case "text/plain":
|
||||
// Assume UTF-8 encoded string.
|
||||
data = string(in.Data)
|
||||
default:
|
||||
if strings.HasPrefix(mediaType, "application/") &&
|
||||
strings.HasSuffix(mediaType, "+json") {
|
||||
if err := json.Unmarshal(in.Data, &v); err == nil {
|
||||
data = v
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
e := &common.TopicEvent{
|
||||
ID: in.Id,
|
||||
Source: in.Source,
|
||||
Type: in.Type,
|
||||
SpecVersion: in.SpecVersion,
|
||||
DataContentType: in.DataContentType,
|
||||
Data: in.Data,
|
||||
Data: data,
|
||||
RawData: in.Data,
|
||||
Topic: in.Topic,
|
||||
PubsubName: in.PubsubName,
|
||||
}
|
||||
|
|
|
@ -161,3 +161,79 @@ func eventHandlerWithRetryError(ctx context.Context, event *common.TopicEvent) (
|
|||
func eventHandlerWithError(ctx context.Context, event *common.TopicEvent) (retry bool, err error) {
|
||||
return false, errors.New("nil event")
|
||||
}
|
||||
|
||||
func TestEventDataHandling(t *testing.T) {
|
||||
ctx := context.Background()
|
||||
|
||||
tests := map[string]struct {
|
||||
contentType string
|
||||
data string
|
||||
value interface{}
|
||||
}{
|
||||
"JSON bytes": {
|
||||
contentType: "application/json",
|
||||
data: `{"message":"hello"}`,
|
||||
value: map[string]interface{}{
|
||||
"message": "hello",
|
||||
},
|
||||
},
|
||||
"JSON entension media type bytes": {
|
||||
contentType: "application/extension+json",
|
||||
data: `{"message":"hello"}`,
|
||||
value: map[string]interface{}{
|
||||
"message": "hello",
|
||||
},
|
||||
},
|
||||
"Test": {
|
||||
contentType: "text/plain",
|
||||
data: `message = hello`,
|
||||
value: `message = hello`,
|
||||
},
|
||||
"Other": {
|
||||
contentType: "application/octet-stream",
|
||||
data: `message = hello`,
|
||||
value: []byte(`message = hello`),
|
||||
},
|
||||
}
|
||||
|
||||
s := getTestServer()
|
||||
|
||||
sub := &common.Subscription{
|
||||
PubsubName: "messages",
|
||||
Topic: "test",
|
||||
Route: "/test",
|
||||
Metadata: map[string]string{},
|
||||
}
|
||||
|
||||
recv := make(chan struct{}, 1)
|
||||
var topicEvent *common.TopicEvent
|
||||
handler := func(ctx context.Context, e *common.TopicEvent) (retry bool, err error) {
|
||||
topicEvent = e
|
||||
recv <- struct{}{}
|
||||
|
||||
return false, nil
|
||||
}
|
||||
err := s.AddTopicEventHandler(sub, handler)
|
||||
assert.NoErrorf(t, err, "error adding event handler")
|
||||
|
||||
startTestServer(s)
|
||||
|
||||
for name, tt := range tests {
|
||||
t.Run(name, func(t *testing.T) {
|
||||
in := runtime.TopicEventRequest{
|
||||
Id: "a123",
|
||||
Source: "test",
|
||||
Type: "test",
|
||||
SpecVersion: "v1.0",
|
||||
DataContentType: tt.contentType,
|
||||
Data: []byte(tt.data),
|
||||
Topic: sub.Topic,
|
||||
PubsubName: sub.PubsubName,
|
||||
}
|
||||
|
||||
s.OnTopicEvent(ctx, &in)
|
||||
<-recv
|
||||
assert.Equal(t, tt.value, topicEvent.Data)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
|
|
@ -2,6 +2,7 @@ package http
|
|||
|
||||
import (
|
||||
"context"
|
||||
"encoding/base64"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"io/ioutil"
|
||||
|
@ -29,6 +30,34 @@ const (
|
|||
PubSubHandlerDropStatusCode int = http.StatusSeeOther
|
||||
)
|
||||
|
||||
// topicEventJSON is identical to `common.TopicEvent`
|
||||
// except for it treats `data` as a json.RawMessage so it can
|
||||
// be used as bytes or interface{}.
|
||||
type topicEventJSON struct {
|
||||
// ID identifies the event.
|
||||
ID string `json:"id"`
|
||||
// The version of the CloudEvents specification.
|
||||
SpecVersion string `json:"specversion"`
|
||||
// The type of event related to the originating occurrence.
|
||||
Type string `json:"type"`
|
||||
// Source identifies the context in which an event happened.
|
||||
Source string `json:"source"`
|
||||
// The content type of data value.
|
||||
DataContentType string `json:"datacontenttype"`
|
||||
// The content of the event.
|
||||
// Note, this is why the gRPC and HTTP implementations need separate structs for cloud events.
|
||||
Data json.RawMessage `json:"data"`
|
||||
// The base64 encoding content of the event.
|
||||
// Note, this is processing rawPayload and binary content types.
|
||||
DataBase64 string `json:"data_base64,omitempty"`
|
||||
// Cloud event subject
|
||||
Subject string `json:"subject"`
|
||||
// The pubsub topic which publisher sent to.
|
||||
Topic string `json:"topic"`
|
||||
// PubsubName is name of the pub/sub this message came from
|
||||
PubsubName string `json:"pubsubname"`
|
||||
}
|
||||
|
||||
func (s *Server) registerBaseHandler() {
|
||||
// register subscribe handler
|
||||
f := func(w http.ResponseWriter, r *http.Request) {
|
||||
|
@ -168,21 +197,78 @@ func (s *Server) AddTopicEventHandler(sub *common.Subscription, fn func(ctx cont
|
|||
}
|
||||
|
||||
// deserialize the event
|
||||
var in common.TopicEvent
|
||||
var in topicEventJSON
|
||||
if err := json.NewDecoder(r.Body).Decode(&in); err != nil {
|
||||
http.Error(w, err.Error(), PubSubHandlerDropStatusCode)
|
||||
return
|
||||
}
|
||||
|
||||
if in.PubsubName == "" {
|
||||
in.Topic = sub.PubsubName
|
||||
}
|
||||
if in.Topic == "" {
|
||||
in.Topic = sub.Topic
|
||||
}
|
||||
|
||||
var data interface{}
|
||||
var rawData []byte
|
||||
if len(in.Data) > 0 {
|
||||
rawData = []byte(in.Data)
|
||||
data = rawData
|
||||
var v interface{}
|
||||
// We can assume that rawData is valid JSON
|
||||
// without checking in.DataContentType == "application/json".
|
||||
if err := json.Unmarshal(rawData, &v); err == nil {
|
||||
data = v
|
||||
// Handling of JSON base64 encoded or escaped in a string.
|
||||
if str, ok := v.(string); ok {
|
||||
// This is the path that will most likely succeed.
|
||||
var vString interface{}
|
||||
if err := json.Unmarshal([]byte(str), &vString); err == nil {
|
||||
data = vString
|
||||
} else if decoded, err := base64.StdEncoding.DecodeString(str); err == nil {
|
||||
// Decoded Base64 encoded JSON does not seem to be in the spec
|
||||
// but it is in existing unit tests so this handles that case.
|
||||
var vBase64 interface{}
|
||||
if err := json.Unmarshal(decoded, &vBase64); err == nil {
|
||||
data = vBase64
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
} else if in.DataBase64 != "" {
|
||||
var err error
|
||||
rawData, err = base64.StdEncoding.DecodeString(in.DataBase64)
|
||||
if err == nil {
|
||||
data = rawData
|
||||
if in.DataContentType == "application/json" {
|
||||
var v interface{}
|
||||
if err := json.Unmarshal(rawData, &v); err == nil {
|
||||
data = v
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
te := common.TopicEvent{
|
||||
ID: in.ID,
|
||||
SpecVersion: in.SpecVersion,
|
||||
Type: in.Type,
|
||||
Source: in.Source,
|
||||
DataContentType: in.DataContentType,
|
||||
Data: data,
|
||||
RawData: rawData,
|
||||
DataBase64: in.DataBase64,
|
||||
Subject: in.Subject,
|
||||
PubsubName: in.PubsubName,
|
||||
Topic: in.Topic,
|
||||
}
|
||||
|
||||
w.Header().Add("Content-Type", "application/json")
|
||||
w.WriteHeader(http.StatusOK)
|
||||
|
||||
// execute user handler
|
||||
retry, err := fn(r.Context(), &in)
|
||||
retry, err := fn(r.Context(), &te)
|
||||
if err == nil {
|
||||
writeStatus(w, common.SubscriptionResponseStatusSuccess)
|
||||
return
|
||||
|
|
|
@ -85,6 +85,129 @@ func TestEventHandler(t *testing.T) {
|
|||
makeEventRequest(t, s, "/errors", data, http.StatusOK)
|
||||
}
|
||||
|
||||
func TestEventDataHandling(t *testing.T) {
|
||||
tests := map[string]struct {
|
||||
data string
|
||||
result interface{}
|
||||
}{
|
||||
"JSON nested": {
|
||||
data: `{
|
||||
"specversion" : "1.0",
|
||||
"type" : "com.github.pull.create",
|
||||
"source" : "https://github.com/cloudevents/spec/pull",
|
||||
"subject" : "123",
|
||||
"id" : "A234-1234-1234",
|
||||
"time" : "2018-04-05T17:31:00Z",
|
||||
"comexampleextension1" : "value",
|
||||
"comexampleothervalue" : 5,
|
||||
"datacontenttype" : "application/json",
|
||||
"data" : {
|
||||
"message":"hello"
|
||||
}
|
||||
}`,
|
||||
result: map[string]interface{}{
|
||||
"message": "hello",
|
||||
},
|
||||
},
|
||||
"JSON base64 encoded in data": {
|
||||
data: `{
|
||||
"specversion" : "1.0",
|
||||
"type" : "com.github.pull.create",
|
||||
"source" : "https://github.com/cloudevents/spec/pull",
|
||||
"subject" : "123",
|
||||
"id" : "A234-1234-1234",
|
||||
"time" : "2018-04-05T17:31:00Z",
|
||||
"comexampleextension1" : "value",
|
||||
"comexampleothervalue" : 5,
|
||||
"datacontenttype" : "application/json",
|
||||
"data" : "eyJtZXNzYWdlIjoiaGVsbG8ifQ=="
|
||||
}`,
|
||||
result: map[string]interface{}{
|
||||
"message": "hello",
|
||||
},
|
||||
},
|
||||
"JSON base64 encoded in data_base64": {
|
||||
data: `{
|
||||
"specversion" : "1.0",
|
||||
"type" : "com.github.pull.create",
|
||||
"source" : "https://github.com/cloudevents/spec/pull",
|
||||
"subject" : "123",
|
||||
"id" : "A234-1234-1234",
|
||||
"time" : "2018-04-05T17:31:00Z",
|
||||
"comexampleextension1" : "value",
|
||||
"comexampleothervalue" : 5,
|
||||
"datacontenttype" : "application/json",
|
||||
"data_base64" : "eyJtZXNzYWdlIjoiaGVsbG8ifQ=="
|
||||
}`,
|
||||
result: map[string]interface{}{
|
||||
"message": "hello",
|
||||
},
|
||||
},
|
||||
"Binary base64 encoded in data_base64": {
|
||||
data: `{
|
||||
"specversion" : "1.0",
|
||||
"type" : "com.github.pull.create",
|
||||
"source" : "https://github.com/cloudevents/spec/pull",
|
||||
"subject" : "123",
|
||||
"id" : "A234-1234-1234",
|
||||
"time" : "2018-04-05T17:31:00Z",
|
||||
"comexampleextension1" : "value",
|
||||
"comexampleothervalue" : 5,
|
||||
"datacontenttype" : "application/octet-stream",
|
||||
"data_base64" : "eyJtZXNzYWdlIjoiaGVsbG8ifQ=="
|
||||
}`,
|
||||
result: []byte(`{"message":"hello"}`),
|
||||
},
|
||||
"JSON string escaped": {
|
||||
data: `{
|
||||
"specversion" : "1.0",
|
||||
"type" : "com.github.pull.create",
|
||||
"source" : "https://github.com/cloudevents/spec/pull",
|
||||
"subject" : "123",
|
||||
"id" : "A234-1234-1234",
|
||||
"time" : "2018-04-05T17:31:00Z",
|
||||
"comexampleextension1" : "value",
|
||||
"comexampleothervalue" : 5,
|
||||
"datacontenttype" : "application/json",
|
||||
"data" : "{\"message\":\"hello\"}"
|
||||
}`,
|
||||
result: map[string]interface{}{
|
||||
"message": "hello",
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
s := newServer("", nil)
|
||||
|
||||
sub := &common.Subscription{
|
||||
PubsubName: "messages",
|
||||
Topic: "test",
|
||||
Route: "/test",
|
||||
Metadata: map[string]string{},
|
||||
}
|
||||
|
||||
recv := make(chan struct{}, 1)
|
||||
var topicEvent *common.TopicEvent
|
||||
handler := func(ctx context.Context, e *common.TopicEvent) (retry bool, err error) {
|
||||
topicEvent = e
|
||||
recv <- struct{}{}
|
||||
|
||||
return false, nil
|
||||
}
|
||||
err := s.AddTopicEventHandler(sub, handler)
|
||||
assert.NoErrorf(t, err, "error adding event handler")
|
||||
|
||||
s.registerBaseHandler()
|
||||
|
||||
for name, tt := range tests {
|
||||
t.Run(name, func(t *testing.T) {
|
||||
makeEventRequest(t, s, "/test", tt.data, http.StatusOK)
|
||||
<-recv
|
||||
assert.Equal(t, tt.result, topicEvent.Data)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestHealthCheck(t *testing.T) {
|
||||
s := newServer("", nil)
|
||||
s.registerBaseHandler()
|
||||
|
|
Loading…
Reference in New Issue