go-sdk/service/http/topic.go

294 lines
9.1 KiB
Go

package http
import (
"context"
"encoding/base64"
"encoding/json"
"fmt"
"io/ioutil"
"net/http"
"strings"
"github.com/gorilla/mux"
actorErr "github.com/dapr/go-sdk/actor/error"
"github.com/dapr/go-sdk/actor/runtime"
"github.com/pkg/errors"
"github.com/dapr/go-sdk/service/common"
)
const (
// PubSubHandlerSuccessStatusCode is the successful ack code for pubsub event appcallback response.
PubSubHandlerSuccessStatusCode int = http.StatusOK
// PubSubHandlerRetryStatusCode is the error response code (nack) pubsub event appcallback response.
PubSubHandlerRetryStatusCode int = http.StatusInternalServerError
// PubSubHandlerDropStatusCode is the pubsub event appcallback response code indicating that Dapr should drop that message.
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) {
w.Header().Set("Content-Type", "application/json")
if err := json.NewEncoder(w).Encode(s.topicSubscriptions); err != nil {
http.Error(w, err.Error(), http.StatusInternalServerError)
return
}
}
s.mux.HandleFunc("/dapr/subscribe", f)
// register health check handler
fHealth := func(w http.ResponseWriter, r *http.Request) {
w.WriteHeader(http.StatusOK)
}
s.mux.HandleFunc("/healthz", fHealth).Methods(http.MethodGet)
// register actor config handler
fRegister := func(w http.ResponseWriter, r *http.Request) {
data, err := runtime.GetActorRuntimeInstance().GetJSONSerializedConfig()
if err != nil {
w.WriteHeader(http.StatusInternalServerError)
return
}
if _, err = w.Write(data); err != nil {
w.WriteHeader(http.StatusInternalServerError)
return
}
w.WriteHeader(http.StatusOK)
}
s.mux.HandleFunc("/dapr/config", fRegister).Methods(http.MethodGet)
// register actor method invoke handler
fInvoke := func(w http.ResponseWriter, r *http.Request) {
varsMap := mux.Vars(r)
actorType := varsMap["actorType"]
actorID := varsMap["actorId"]
methodName := varsMap["methodName"]
reqData, _ := ioutil.ReadAll(r.Body)
rspData, err := runtime.GetActorRuntimeInstance().InvokeActorMethod(actorType, actorID, methodName, reqData)
if err == actorErr.ErrActorTypeNotFound {
w.WriteHeader(http.StatusNotFound)
return
}
if err != actorErr.Success {
w.WriteHeader(http.StatusInternalServerError)
return
}
w.WriteHeader(http.StatusOK)
_, _ = w.Write(rspData)
}
s.mux.HandleFunc("/actors/{actorType}/{actorId}/method/{methodName}", fInvoke).Methods(http.MethodPut)
// register deactivate actor handler
fDelete := func(w http.ResponseWriter, r *http.Request) {
varsMap := mux.Vars(r)
actorType := varsMap["actorType"]
actorID := varsMap["actorId"]
err := runtime.GetActorRuntimeInstance().Deactivate(actorType, actorID)
if err == actorErr.ErrActorTypeNotFound || err == actorErr.ErrActorIDNotFound {
w.WriteHeader(http.StatusNotFound)
}
if err != actorErr.Success {
w.WriteHeader(http.StatusInternalServerError)
}
w.WriteHeader(http.StatusOK)
}
s.mux.HandleFunc("/actors/{actorType}/{actorId}", fDelete).Methods(http.MethodDelete)
// register actor reminder invoke handler
fReminder := func(w http.ResponseWriter, r *http.Request) {
varsMap := mux.Vars(r)
actorType := varsMap["actorType"]
actorID := varsMap["actorId"]
reminderName := varsMap["reminderName"]
reqData, _ := ioutil.ReadAll(r.Body)
err := runtime.GetActorRuntimeInstance().InvokeReminder(actorType, actorID, reminderName, reqData)
if err == actorErr.ErrActorTypeNotFound {
w.WriteHeader(http.StatusNotFound)
}
if err != actorErr.Success {
w.WriteHeader(http.StatusInternalServerError)
}
w.WriteHeader(http.StatusOK)
}
s.mux.HandleFunc("/actors/{actorType}/{actorId}/method/remind/{reminderName}", fReminder).Methods(http.MethodPut)
// register actor timer invoke handler
fTimer := func(w http.ResponseWriter, r *http.Request) {
varsMap := mux.Vars(r)
actorType := varsMap["actorType"]
actorID := varsMap["actorId"]
timerName := varsMap["timerName"]
reqData, _ := ioutil.ReadAll(r.Body)
err := runtime.GetActorRuntimeInstance().InvokeTimer(actorType, actorID, timerName, reqData)
if err == actorErr.ErrActorTypeNotFound {
w.WriteHeader(http.StatusNotFound)
}
if err != actorErr.Success {
w.WriteHeader(http.StatusInternalServerError)
}
w.WriteHeader(http.StatusOK)
}
s.mux.HandleFunc("/actors/{actorType}/{actorId}/method/timer/{timerName}", fTimer).Methods(http.MethodPut)
}
// AddTopicEventHandler appends provided event handler with it's name to the service.
func (s *Server) AddTopicEventHandler(sub *common.Subscription, fn func(ctx context.Context, e *common.TopicEvent) (retry bool, err error)) error {
if sub == nil {
return errors.New("subscription required")
}
if sub.Topic == "" {
return errors.New("topic name required")
}
if sub.PubsubName == "" {
return errors.New("pub/sub name required")
}
if sub.Route == "" {
return errors.New("handler route name")
}
if fn == nil {
return fmt.Errorf("topic handler required")
}
if !strings.HasPrefix(sub.Route, "/") {
sub.Route = fmt.Sprintf("/%s", sub.Route)
}
s.topicSubscriptions = append(s.topicSubscriptions, sub)
s.mux.Handle(sub.Route, optionsHandler(http.HandlerFunc(
func(w http.ResponseWriter, r *http.Request) {
// check for post with no data
if r.ContentLength == 0 {
http.Error(w, "nil content", PubSubHandlerDropStatusCode)
return
}
// deserialize the event
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(), &te)
if err == nil {
writeStatus(w, common.SubscriptionResponseStatusSuccess)
return
}
if retry {
writeStatus(w, common.SubscriptionResponseStatusRetry)
return
}
writeStatus(w, common.SubscriptionResponseStatusDrop)
})))
return nil
}
func writeStatus(w http.ResponseWriter, s string) {
status := &common.SubscriptionResponse{Status: s}
if err := json.NewEncoder(w).Encode(status); err != nil {
http.Error(w, err.Error(), PubSubHandlerRetryStatusCode)
}
}