snapshot of https://github.com/centrifugal/centrifugo.git (master, 2026-09-16; blobs > 131072B omitted)
Some checks failed
test / Lint (push) Failing after 3s
test / Test with Go 1.26.8 (push) Failing after 2s

This commit is contained in:
upstream-mirror
2026-09-16 01:17:21 +05:00
commit b352a9a30d
472 changed files with 108375 additions and 0 deletions

1340
internal/api/api.go Normal file

File diff suppressed because it is too large Load Diff

747
internal/api/api_test.go Normal file
View File

@@ -0,0 +1,747 @@
package api
import (
"context"
"encoding/base64"
"fmt"
"os"
"testing"
"time"
. "github.com/centrifugal/centrifugo/v6/internal/apiproto"
"github.com/centrifugal/centrifugo/v6/internal/config"
"github.com/centrifugal/centrifugo/v6/internal/configtypes"
"github.com/centrifugal/centrifugo/v6/internal/metrics"
"github.com/centrifugal/centrifuge"
"github.com/prometheus/client_golang/prometheus"
"github.com/stretchr/testify/require"
)
func TestMain(m *testing.M) {
// Initialize metrics with a custom registry for tests to avoid conflicts
registry := prometheus.NewRegistry()
_ = metrics.Init(metrics.Config{
Registerer: registry,
})
os.Exit(m.Run())
}
func nodeWithMemoryEngine() *centrifuge.Node {
n, err := centrifuge.New(centrifuge.Config{})
if err != nil {
panic(err)
}
err = n.Run()
if err != nil {
panic(err)
}
return n
}
type testSurveyCaller struct{}
func (t testSurveyCaller) Channels(_ context.Context, _ *ChannelsRequest) (map[string]*ChannelInfo, error) {
return nil, nil
}
func (t testSurveyCaller) Connections(_ context.Context, _ *ConnectionsRequest) (map[string]*ConnectionInfo, error) {
return nil, nil
}
func TestPublishAPI(t *testing.T) {
node := nodeWithMemoryEngine()
cfg := config.DefaultConfig()
cfgContainer, err := config.NewContainer(cfg)
require.NoError(t, err)
api := NewExecutor(node, cfgContainer, &testSurveyCaller{}, ExecutorConfig{Protocol: "test", UseOpenTelemetry: false})
resp := api.Publish(context.Background(), &PublishRequest{})
require.Equal(t, ErrorBadRequest, resp.Error)
resp = api.Publish(context.Background(), &PublishRequest{Channel: "test"})
require.Equal(t, ErrorBadRequest, resp.Error)
resp = api.Publish(context.Background(), &PublishRequest{Channel: "test", Data: []byte("test")})
require.Nil(t, resp.Error)
resp = api.Publish(context.Background(), &PublishRequest{Channel: "test:test", Data: []byte("test")})
require.Equal(t, ErrorUnknownChannel, resp.Error)
}
// TestBatchAPINilCommand guards against a crash on a nil batch command (e.g. a
// JSON `null` array element). In parallel mode each command runs in its own
// goroutine, so a nil-deref would escape net/http's recover and crash the node.
func TestBatchAPINilCommand(t *testing.T) {
node := nodeWithMemoryEngine()
cfg := config.DefaultConfig()
cfgContainer, err := config.NewContainer(cfg)
require.NoError(t, err)
api := NewExecutor(node, cfgContainer, &testSurveyCaller{}, ExecutorConfig{Protocol: "test", UseOpenTelemetry: false})
for _, parallel := range []bool{false, true} {
resp := api.Batch(context.Background(), &BatchRequest{
Parallel: parallel,
Commands: []*Command{
nil,
{Publish: &PublishRequest{Channel: "test", Data: []byte("x")}},
},
})
require.Len(t, resp.Replies, 2)
require.Equal(t, ErrorBadRequest, resp.Replies[0].Error)
require.Nil(t, resp.Replies[1].Error)
}
}
func TestBroadcastAPI(t *testing.T) {
node := nodeWithMemoryEngine()
cfg := config.DefaultConfig()
cfgContainer, err := config.NewContainer(cfg)
require.NoError(t, err)
api := NewExecutor(node, cfgContainer, &testSurveyCaller{}, ExecutorConfig{Protocol: "test", UseOpenTelemetry: false})
resp := api.Broadcast(context.Background(), &BroadcastRequest{})
require.Equal(t, ErrorBadRequest, resp.Error)
// Empty data validation now happens per-channel
resp = api.Broadcast(context.Background(), &BroadcastRequest{Channels: []string{"test"}})
require.Nil(t, resp.Error)
require.Equal(t, ErrorBadRequest.Code, resp.Result.Responses[0].Error.Code)
resp = api.Broadcast(context.Background(), &BroadcastRequest{Channels: []string{"test"}, Data: []byte("test")})
require.Nil(t, resp.Error)
resp = api.Broadcast(context.Background(), &BroadcastRequest{Channels: []string{"test:test"}, Data: []byte("test")})
require.Nil(t, resp.Error)
require.Equal(t, ErrorUnknownChannel, resp.Result.Responses[0].Error)
resp = api.Broadcast(context.Background(), &BroadcastRequest{Channels: []string{"test", "test:test"}, Data: []byte("test")})
require.Nil(t, resp.Error)
require.Equal(t, ErrorUnknownChannel, resp.Result.Responses[1].Error)
}
func TestPublishAPI_DataFormat_JSON(t *testing.T) {
node := nodeWithMemoryEngine()
defer func() { _ = node.Shutdown(context.Background()) }()
cfg := config.DefaultConfig()
cfg.Channel.WithoutNamespace.PublicationDataFormat = configtypes.PublicationDataFormatJSON
cfgContainer, err := config.NewContainer(cfg)
require.NoError(t, err)
api := NewExecutor(node, cfgContainer, &testSurveyCaller{}, ExecutorConfig{Protocol: "test", UseOpenTelemetry: false})
// Valid JSON should succeed
resp := api.Publish(context.Background(), &PublishRequest{
Channel: "test",
Data: []byte(`{"key": "value"}`),
})
require.Nil(t, resp.Error)
// Valid JSON array should succeed
resp = api.Publish(context.Background(), &PublishRequest{
Channel: "test",
Data: []byte(`[1, 2, 3]`),
})
require.Nil(t, resp.Error)
// Valid JSON primitives should succeed
resp = api.Publish(context.Background(), &PublishRequest{
Channel: "test",
Data: []byte(`"string"`),
})
require.Nil(t, resp.Error)
resp = api.Publish(context.Background(), &PublishRequest{
Channel: "test",
Data: []byte(`123`),
})
require.Nil(t, resp.Error)
resp = api.Publish(context.Background(), &PublishRequest{
Channel: "test",
Data: []byte(`true`),
})
require.Nil(t, resp.Error)
resp = api.Publish(context.Background(), &PublishRequest{
Channel: "test",
Data: []byte(`null`),
})
require.Nil(t, resp.Error)
// Invalid JSON should fail
resp = api.Publish(context.Background(), &PublishRequest{
Channel: "test",
Data: []byte(`not valid json`),
})
require.NotNil(t, resp.Error)
require.Equal(t, ErrorBadRequest.Code, resp.Error.Code)
// Empty data with JSON format should fail
resp = api.Publish(context.Background(), &PublishRequest{
Channel: "test",
Data: []byte{},
})
require.NotNil(t, resp.Error)
require.Equal(t, ErrorBadRequest.Code, resp.Error.Code)
}
func TestPublishAPI_DataFormat_Binary(t *testing.T) {
node := nodeWithMemoryEngine()
defer func() { _ = node.Shutdown(context.Background()) }()
cfg := config.DefaultConfig()
cfg.Channel.WithoutNamespace.PublicationDataFormat = configtypes.PublicationDataFormatBinary
cfgContainer, err := config.NewContainer(cfg)
require.NoError(t, err)
api := NewExecutor(node, cfgContainer, &testSurveyCaller{}, ExecutorConfig{Protocol: "test", UseOpenTelemetry: false})
// Binary data should succeed
resp := api.Publish(context.Background(), &PublishRequest{
Channel: "test",
Data: []byte{0x01, 0x02, 0x03},
})
require.Nil(t, resp.Error)
// Empty data with binary format should succeed
resp = api.Publish(context.Background(), &PublishRequest{
Channel: "test",
Data: []byte{},
})
require.Nil(t, resp.Error)
// Any data should succeed with binary format
resp = api.Publish(context.Background(), &PublishRequest{
Channel: "test",
Data: []byte("any text data"),
})
require.Nil(t, resp.Error)
}
func TestPublishAPI_DataFormat_Default(t *testing.T) {
node := nodeWithMemoryEngine()
defer func() { _ = node.Shutdown(context.Background()) }()
cfg := config.DefaultConfig()
// Default format (empty string) - should reject empty data
cfgContainer, err := config.NewContainer(cfg)
require.NoError(t, err)
api := NewExecutor(node, cfgContainer, &testSurveyCaller{}, ExecutorConfig{Protocol: "test", UseOpenTelemetry: false})
// Non-empty data should succeed
resp := api.Publish(context.Background(), &PublishRequest{
Channel: "test",
Data: []byte("any data"),
})
require.Nil(t, resp.Error)
// Empty data should fail (current behavior preserved)
resp = api.Publish(context.Background(), &PublishRequest{
Channel: "test",
Data: []byte{},
})
require.NotNil(t, resp.Error)
require.Equal(t, ErrorBadRequest.Code, resp.Error.Code)
}
func TestBroadcastAPI_DataFormat_JSON(t *testing.T) {
node := nodeWithMemoryEngine()
defer func() { _ = node.Shutdown(context.Background()) }()
cfg := config.DefaultConfig()
cfg.Channel.WithoutNamespace.PublicationDataFormat = configtypes.PublicationDataFormatJSON
cfgContainer, err := config.NewContainer(cfg)
require.NoError(t, err)
api := NewExecutor(node, cfgContainer, &testSurveyCaller{}, ExecutorConfig{Protocol: "test", UseOpenTelemetry: false})
// Valid JSON should succeed
resp := api.Broadcast(context.Background(), &BroadcastRequest{
Channels: []string{"test", "test2"},
Data: []byte(`{"key": "value"}`),
})
require.Nil(t, resp.Error)
require.Nil(t, resp.Result.Responses[0].Error)
require.Nil(t, resp.Result.Responses[1].Error)
// Invalid JSON should fail for all channels
resp = api.Broadcast(context.Background(), &BroadcastRequest{
Channels: []string{"test", "test2"},
Data: []byte(`not valid json`),
})
require.Nil(t, resp.Error)
require.Equal(t, ErrorBadRequest.Code, resp.Result.Responses[0].Error.Code)
require.Equal(t, ErrorBadRequest.Code, resp.Result.Responses[1].Error.Code)
}
func TestBroadcastAPI_DataFormat_Binary(t *testing.T) {
node := nodeWithMemoryEngine()
defer func() { _ = node.Shutdown(context.Background()) }()
cfg := config.DefaultConfig()
cfg.Channel.WithoutNamespace.PublicationDataFormat = configtypes.PublicationDataFormatBinary
cfgContainer, err := config.NewContainer(cfg)
require.NoError(t, err)
api := NewExecutor(node, cfgContainer, &testSurveyCaller{}, ExecutorConfig{Protocol: "test", UseOpenTelemetry: false})
// Empty data should succeed with binary format
resp := api.Broadcast(context.Background(), &BroadcastRequest{
Channels: []string{"test", "test2"},
Data: []byte{},
})
require.Nil(t, resp.Error)
require.Nil(t, resp.Result.Responses[0].Error)
require.Nil(t, resp.Result.Responses[1].Error)
}
func TestHistoryAPI(t *testing.T) {
node := nodeWithMemoryEngine()
cfg := config.DefaultConfig()
cfgContainer, err := config.NewContainer(cfg)
require.NoError(t, err)
api := NewExecutor(node, cfgContainer, &testSurveyCaller{}, ExecutorConfig{Protocol: "test", UseOpenTelemetry: false})
resp := api.History(context.Background(), &HistoryRequest{})
require.Equal(t, ErrorBadRequest, resp.Error)
resp = api.History(context.Background(), &HistoryRequest{Channel: "test"})
require.Equal(t, ErrorNotAvailable, resp.Error)
cfg = cfgContainer.Config()
cfg.Channel.WithoutNamespace.HistorySize = 1
cfg.Channel.WithoutNamespace.HistoryTTL = configtypes.Duration(1 * time.Second)
_ = cfgContainer.Reload(cfg)
resp = api.History(context.Background(), &HistoryRequest{Channel: "test"})
require.Nil(t, resp.Error)
}
func TestHistoryRemoveAPI(t *testing.T) {
node := nodeWithMemoryEngine()
cfg := config.DefaultConfig()
cfgContainer, err := config.NewContainer(cfg)
require.NoError(t, err)
api := NewExecutor(node, cfgContainer, &testSurveyCaller{}, ExecutorConfig{Protocol: "test", UseOpenTelemetry: false})
resp := api.HistoryRemove(context.Background(), &HistoryRemoveRequest{})
require.Equal(t, ErrorBadRequest, resp.Error)
resp = api.HistoryRemove(context.Background(), &HistoryRemoveRequest{Channel: "test"})
require.Equal(t, ErrorNotAvailable, resp.Error)
cfg = cfgContainer.Config()
cfg.Channel.WithoutNamespace.HistorySize = 1
cfg.Channel.WithoutNamespace.HistoryTTL = configtypes.Duration(time.Second)
_ = cfgContainer.Reload(cfg)
resp = api.HistoryRemove(context.Background(), &HistoryRemoveRequest{Channel: "test"})
require.Nil(t, resp.Error)
}
func TestPresenceAPI(t *testing.T) {
node := nodeWithMemoryEngine()
cfg := config.DefaultConfig()
cfgContainer, err := config.NewContainer(cfg)
require.NoError(t, err)
api := NewExecutor(node, cfgContainer, &testSurveyCaller{}, ExecutorConfig{Protocol: "test", UseOpenTelemetry: false})
resp := api.Presence(context.Background(), &PresenceRequest{})
require.Equal(t, ErrorBadRequest, resp.Error)
resp = api.Presence(context.Background(), &PresenceRequest{Channel: "test"})
require.Equal(t, ErrorNotAvailable, resp.Error)
cfg = cfgContainer.Config()
cfg.Channel.WithoutNamespace.Presence = true
_ = cfgContainer.Reload(cfg)
resp = api.Presence(context.Background(), &PresenceRequest{Channel: "test"})
require.Nil(t, resp.Error)
}
func TestPresenceStatsAPI(t *testing.T) {
node := nodeWithMemoryEngine()
cfg := config.DefaultConfig()
cfgContainer, err := config.NewContainer(cfg)
require.NoError(t, err)
api := NewExecutor(node, cfgContainer, &testSurveyCaller{}, ExecutorConfig{Protocol: "test", UseOpenTelemetry: false})
resp := api.PresenceStats(context.Background(), &PresenceStatsRequest{})
require.Equal(t, ErrorBadRequest, resp.Error)
resp = api.PresenceStats(context.Background(), &PresenceStatsRequest{Channel: "test"})
require.Equal(t, ErrorNotAvailable, resp.Error)
cfg = cfgContainer.Config()
cfg.Channel.WithoutNamespace.Presence = true
_ = cfgContainer.Reload(cfg)
resp = api.PresenceStats(context.Background(), &PresenceStatsRequest{Channel: "test"})
require.Nil(t, resp.Error)
}
func TestDisconnectAPI(t *testing.T) {
node := nodeWithMemoryEngine()
cfg := config.DefaultConfig()
cfgContainer, err := config.NewContainer(cfg)
require.NoError(t, err)
api := NewExecutor(node, cfgContainer, &testSurveyCaller{}, ExecutorConfig{Protocol: "test", UseOpenTelemetry: false})
resp := api.Disconnect(context.Background(), &DisconnectRequest{
User: "test",
})
require.Nil(t, resp.Error)
}
func TestUnsubscribeAPI(t *testing.T) {
node := nodeWithMemoryEngine()
cfg := config.DefaultConfig()
cfgContainer, err := config.NewContainer(cfg)
require.NoError(t, err)
api := NewExecutor(node, cfgContainer, &testSurveyCaller{}, ExecutorConfig{Protocol: "test", UseOpenTelemetry: false})
resp := api.Unsubscribe(context.Background(), &UnsubscribeRequest{
User: "test",
Channel: "test",
})
require.Nil(t, resp.Error)
}
func TestRefreshAPI(t *testing.T) {
node := nodeWithMemoryEngine()
cfg := config.DefaultConfig()
cfgContainer, err := config.NewContainer(cfg)
require.NoError(t, err)
api := NewExecutor(node, cfgContainer, &testSurveyCaller{}, ExecutorConfig{Protocol: "test", UseOpenTelemetry: false})
resp := api.Refresh(context.Background(), &RefreshRequest{
User: "test",
})
require.Nil(t, resp.Error)
}
func TestSubscribeAPI(t *testing.T) {
node := nodeWithMemoryEngine()
cfg := config.DefaultConfig()
cfgContainer, err := config.NewContainer(cfg)
require.NoError(t, err)
api := NewExecutor(node, cfgContainer, &testSurveyCaller{}, ExecutorConfig{Protocol: "test", UseOpenTelemetry: false})
resp := api.Subscribe(context.Background(), &SubscribeRequest{
User: "test",
Channel: "test",
})
require.Nil(t, resp.Error)
}
func TestInfoAPI(t *testing.T) {
node := nodeWithMemoryEngine()
cfg := config.DefaultConfig()
cfgContainer, err := config.NewContainer(cfg)
require.NoError(t, err)
api := NewExecutor(node, cfgContainer, &testSurveyCaller{}, ExecutorConfig{Protocol: "test", UseOpenTelemetry: false})
resp := api.Info(context.Background(), &InfoRequest{})
require.Nil(t, resp.Error)
}
// configWithNamespace returns a DefaultConfig with a single namespace having the
// given name and subscription_type. Used to test the subscription_type gates on
// publish/map_*/shared_poll_* API endpoints. Fills the minimum mandatory fields
// (map.mode for map types, global shared_poll.hmac_secret_key for shared_poll)
// so the config passes Validate().
func configWithNamespace(name, subscriptionType string) config.Config {
cfg := config.DefaultConfig()
chOpts := configtypes.ChannelOptions{
SubscriptionType: subscriptionType,
}
switch subscriptionType {
case "map":
// Persistent mode requires no key_ttl and is the simplest minimum.
chOpts.Map = configtypes.MapConfig{Mode: "persistent"}
case "map_clients", "map_users":
// Persistent mode is forbidden here (presence entries need TTL-based
// cleanup), so use ephemeral with a key_ttl.
chOpts.Map = configtypes.MapConfig{
Mode: "ephemeral",
KeyTTL: configtypes.Duration(time.Minute),
}
case "shared_poll":
// Global HMAC secret required for any shared_poll namespace.
cfg.SharedPoll.HMACSecretKey = "test-secret"
// Default refresh proxy must have an endpoint when no explicit
// proxy_name is set on the namespace.
cfg.Channel.Proxy.SharedPollRefresh = configtypes.Proxy{
Endpoint: "http://127.0.0.1:0/test",
Timeout: configtypes.Duration(time.Second),
}
chOpts.SharedPoll = configtypes.SharedPollConfig{Mode: "versioned"}
}
cfg.Channel.Namespaces = []configtypes.ChannelNamespace{
{
Name: name,
ChannelOptions: chOpts,
},
}
return cfg
}
// TestPublishAPI_RejectsMapAndSharedPollNamespaces ensures the stream Publish
// endpoint rejects calls aimed at namespaces configured for map or shared poll
// subscriptions — those have their own dedicated APIs.
func TestPublishAPI_RejectsMapAndSharedPollNamespaces(t *testing.T) {
cases := []struct {
subscriptionType string
}{
{"map"},
{"map_clients"},
{"map_users"},
{"shared_poll"},
}
for _, c := range cases {
t.Run(c.subscriptionType, func(t *testing.T) {
node := nodeWithMemoryEngine()
defer func() { _ = node.Shutdown(context.Background()) }()
cfg := configWithNamespace("ns", c.subscriptionType)
cfgContainer, err := config.NewContainer(cfg)
require.NoError(t, err)
api := NewExecutor(node, cfgContainer, &testSurveyCaller{}, ExecutorConfig{Protocol: "test", UseOpenTelemetry: false})
resp := api.Publish(context.Background(), &PublishRequest{Channel: "ns:test", Data: []byte("data")})
require.Equal(t, ErrorBadRequest, resp.Error)
})
}
}
// TestMapPublishAPI_RejectsNonMapNamespaces ensures map_publish rejects calls
// against namespaces that aren't configured for map subscriptions.
func TestMapPublishAPI_RejectsNonMapNamespaces(t *testing.T) {
cases := []struct {
subscriptionType string
}{
{""},
{"stream"},
{"shared_poll"},
}
for _, c := range cases {
t.Run("type="+c.subscriptionType, func(t *testing.T) {
node := nodeWithMemoryEngine()
defer func() { _ = node.Shutdown(context.Background()) }()
cfg := configWithNamespace("ns", c.subscriptionType)
cfgContainer, err := config.NewContainer(cfg)
require.NoError(t, err)
api := NewExecutor(node, cfgContainer, &testSurveyCaller{}, ExecutorConfig{Protocol: "test", UseOpenTelemetry: false})
resp := api.MapPublish(context.Background(), &MapPublishRequest{
Channel: "ns:test",
Key: "k",
Data: []byte(`{"v":1}`),
})
require.Equal(t, ErrorBadRequest, resp.Error)
})
}
}
// TestMapPublishAPI_RejectsInvalidKeyMode ensures key_mode is validated against
// the allowed set ("", "if_new", "if_exists").
func TestMapPublishAPI_RejectsInvalidKeyMode(t *testing.T) {
node := nodeWithMemoryEngine()
defer func() { _ = node.Shutdown(context.Background()) }()
cfg := configWithNamespace("ns", "map")
cfgContainer, err := config.NewContainer(cfg)
require.NoError(t, err)
api := NewExecutor(node, cfgContainer, &testSurveyCaller{}, ExecutorConfig{Protocol: "test", UseOpenTelemetry: false})
resp := api.MapPublish(context.Background(), &MapPublishRequest{
Channel: "ns:test",
Key: "k",
Data: []byte(`{"v":1}`),
KeyMode: "bogus",
})
require.Equal(t, ErrorBadRequest, resp.Error)
}
// TestMapAPIs_RejectNonMapNamespaces ensures the other map API endpoints
// (remove / read_state / read_stream / stats / clear) reject calls against
// namespaces that aren't configured for map subscriptions, instead of leaking
// internal errors from the broker.
func TestMapAPIs_RejectNonMapNamespaces(t *testing.T) {
node := nodeWithMemoryEngine()
defer func() { _ = node.Shutdown(context.Background()) }()
cfg := configWithNamespace("ns", "") // default = stream
cfgContainer, err := config.NewContainer(cfg)
require.NoError(t, err)
api := NewExecutor(node, cfgContainer, &testSurveyCaller{}, ExecutorConfig{Protocol: "test", UseOpenTelemetry: false})
rmResp := api.MapRemove(context.Background(), &MapRemoveRequest{Channel: "ns:test", Key: "k"})
require.Equal(t, ErrorBadRequest, rmResp.Error)
rsResp := api.MapReadState(context.Background(), &MapReadStateRequest{Channel: "ns:test"})
require.Equal(t, ErrorBadRequest, rsResp.Error)
rstrResp := api.MapReadStream(context.Background(), &MapReadStreamRequest{Channel: "ns:test"})
require.Equal(t, ErrorBadRequest, rstrResp.Error)
stResp := api.MapStats(context.Background(), &MapStatsRequest{Channel: "ns:test"})
require.Equal(t, ErrorBadRequest, stResp.Error)
clResp := api.MapClear(context.Background(), &MapClearRequest{Channel: "ns:test"})
require.Equal(t, ErrorBadRequest, clResp.Error)
}
// TestMapAPIs_RejectUnknownChannel ensures the map API endpoints return
// ErrorUnknownChannel instead of leaking internal errors when the channel's
// namespace doesn't exist.
func TestMapAPIs_RejectUnknownChannel(t *testing.T) {
node := nodeWithMemoryEngine()
defer func() { _ = node.Shutdown(context.Background()) }()
cfg := config.DefaultConfig()
cfgContainer, err := config.NewContainer(cfg)
require.NoError(t, err)
api := NewExecutor(node, cfgContainer, &testSurveyCaller{}, ExecutorConfig{Protocol: "test", UseOpenTelemetry: false})
// "missing:test" — namespace "missing" not configured.
pubResp := api.MapPublish(context.Background(), &MapPublishRequest{Channel: "missing:test", Key: "k", Data: []byte(`{}`)})
require.Equal(t, ErrorUnknownChannel, pubResp.Error)
rmResp := api.MapRemove(context.Background(), &MapRemoveRequest{Channel: "missing:test", Key: "k"})
require.Equal(t, ErrorUnknownChannel, rmResp.Error)
rsResp := api.MapReadState(context.Background(), &MapReadStateRequest{Channel: "missing:test"})
require.Equal(t, ErrorUnknownChannel, rsResp.Error)
rstrResp := api.MapReadStream(context.Background(), &MapReadStreamRequest{Channel: "missing:test"})
require.Equal(t, ErrorUnknownChannel, rstrResp.Error)
stResp := api.MapStats(context.Background(), &MapStatsRequest{Channel: "missing:test"})
require.Equal(t, ErrorUnknownChannel, stResp.Error)
clResp := api.MapClear(context.Background(), &MapClearRequest{Channel: "missing:test"})
require.Equal(t, ErrorUnknownChannel, clResp.Error)
}
// nodeWithMapSupport builds a node able to actually serve map operations for
// channels of the given config container, so that valid publishes really reach
// the map broker instead of failing on options resolution.
func nodeWithMapSupport(cfgContainer *config.Container) *centrifuge.Node {
n, err := centrifuge.New(centrifuge.Config{
Map: centrifuge.MapConfig{
GetMapChannelOptions: func(channel string) centrifuge.MapChannelOptions {
_, _, chOpts, ok, err := cfgContainer.ChannelOptions(channel)
if err != nil || !ok {
return centrifuge.MapChannelOptions{}
}
var mode centrifuge.MapMode
switch chOpts.Map.Mode {
case "ephemeral":
mode = centrifuge.MapModeEphemeral
case "recoverable":
mode = centrifuge.MapModeRecoverable
case "persistent":
mode = centrifuge.MapModePersistent
}
return centrifuge.MapChannelOptions{Mode: mode, KeyTTL: chOpts.Map.KeyTTL.ToDuration()}
},
},
})
if err != nil {
panic(err)
}
if err = n.Run(); err != nil {
panic(err)
}
return n
}
// TestMapPublishAPI_DataFormat ensures map_publish applies the namespace
// publication_data_format policy, same as the stream Publish endpoint does.
func TestMapPublishAPI_DataFormat(t *testing.T) {
cases := []struct {
format string
data []byte
b64Data string
wantReject bool
}{
{configtypes.PublicationDataFormatJSONObject, []byte(`{"v":1}`), "", false},
{configtypes.PublicationDataFormatJSONObject, []byte(`[1,2]`), "", true},
{configtypes.PublicationDataFormatJSONObject, []byte(`not-json-object`), "", true},
{configtypes.PublicationDataFormatJSON, []byte(`[1,2]`), "", false},
{configtypes.PublicationDataFormatJSON, []byte(`nope`), "", true},
{configtypes.PublicationDataFormatBinary, nil, "", false},
{"", []byte(`whatever`), "", false},
{"", nil, "", true}, // default format rejects empty data
// b64data must be validated after decoding, not before.
{configtypes.PublicationDataFormatJSONObject, nil, base64.StdEncoding.EncodeToString([]byte(`{"v":1}`)), false},
{configtypes.PublicationDataFormatJSONObject, nil, base64.StdEncoding.EncodeToString([]byte(`nope`)), true},
}
for i, c := range cases {
t.Run(fmt.Sprintf("%d_format=%q", i, c.format), func(t *testing.T) {
cfg := configWithNamespace("ns", "map")
cfg.Channel.Namespaces[0].PublicationDataFormat = c.format
cfgContainer, err := config.NewContainer(cfg)
require.NoError(t, err)
node := nodeWithMapSupport(cfgContainer)
defer func() { _ = node.Shutdown(context.Background()) }()
api := NewExecutor(node, cfgContainer, &testSurveyCaller{}, ExecutorConfig{Protocol: "test"})
resp := api.MapPublish(context.Background(), &MapPublishRequest{
Channel: "ns:test", Key: "k", Data: c.data, B64Data: c.b64Data,
})
if c.wantReject {
require.Equal(t, ErrorBadRequest, resp.Error)
} else {
require.Nil(t, resp.Error)
}
})
}
}
// TestSharedPollPublishAPI_DataFormat ensures shared_poll_publish applies the
// namespace publication_data_format policy.
func TestSharedPollPublishAPI_DataFormat(t *testing.T) {
cases := []struct {
format string
data []byte
wantReject bool
}{
{configtypes.PublicationDataFormatJSONObject, []byte(`{"v":1}`), false},
{configtypes.PublicationDataFormatJSONObject, []byte(`nope`), true},
{configtypes.PublicationDataFormatJSON, []byte(`nope`), true},
{configtypes.PublicationDataFormatBinary, nil, false},
{"", nil, true}, // default format rejects empty data
}
for i, c := range cases {
t.Run(fmt.Sprintf("%d_format=%q", i, c.format), func(t *testing.T) {
cfg := configWithNamespace("ns", "shared_poll")
cfg.Channel.Namespaces[0].PublicationDataFormat = c.format
cfg.Channel.Namespaces[0].SharedPoll.PublishEnabled = true
cfgContainer, err := config.NewContainer(cfg)
require.NoError(t, err)
node := nodeWithMemoryEngine()
defer func() { _ = node.Shutdown(context.Background()) }()
api := NewExecutor(node, cfgContainer, &testSurveyCaller{}, ExecutorConfig{Protocol: "test"})
resp := api.SharedPollPublish(context.Background(), &SharedPollPublishRequest{
Channel: "ns:test", Key: "k", Version: 1, Data: c.data,
})
if c.wantReject {
require.Equal(t, ErrorBadRequest, resp.Error)
} else {
// The test node has no shared poll manager, so a valid payload
// gets past validation and fails later — the point is that it is
// not rejected as a bad request.
require.NotEqual(t, ErrorBadRequest, resp.Error)
}
})
}
}

314
internal/api/consuming.go Normal file
View File

@@ -0,0 +1,314 @@
package api
import (
"context"
"encoding/json"
"errors"
"fmt"
"github.com/centrifugal/centrifugo/v6/internal/apiproto"
"github.com/centrifugal/centrifuge"
"github.com/rs/zerolog/log"
"go.opentelemetry.io/otel/codes"
"go.opentelemetry.io/otel/trace"
)
// ConsumingHandlerConfig configures ConsumingHandler.
type ConsumingHandlerConfig struct {
UseOpenTelemetry bool
}
// ConsumingHandler is responsible for processing API commands from asynchronous consumers.
type ConsumingHandler struct {
node *centrifuge.Node
config ConsumingHandlerConfig
api *Executor
}
// NewConsumingHandler creates new ConsumingHandler.
func NewConsumingHandler(n *centrifuge.Node, apiExecutor *Executor, c ConsumingHandlerConfig) *ConsumingHandler {
h := &ConsumingHandler{
node: n,
config: c,
api: apiExecutor,
}
return h
}
func logNonRetryableConsumingError(err error, method string) {
log.Error().Err(err).Str("method", method).Msg("non retryable error during consuming, skip message")
}
// ErrShutdown is returned to consumers for messages received after Node shutdown
// started. Such messages must not be acknowledged: this node is no longer able to
// deliver them, and depending on the broker a publish may still return success (a
// memory broker never fails, so without this check a message would be committed
// while going nowhere). Returning an error makes consumers leave the message
// unacknowledged, so it's redelivered to another node or after restart.
//
// It wraps context.Canceled since consumers treat cancellation as a stop signal
// and give up immediately instead of retrying with backoff until the shutdown
// timeout is reached.
var ErrShutdown = fmt.Errorf("node is shutting down: %w", context.Canceled)
type Dispatcher struct {
handler *ConsumingHandler
}
func NewDispatcher(handler *ConsumingHandler) *Dispatcher {
return &Dispatcher{
handler: handler,
}
}
// shuttingDown returns true once Node shutdown started.
func (d *Dispatcher) shuttingDown() bool {
select {
case <-d.handler.node.NotifyShutdown():
return true
default:
return false
}
}
func (d *Dispatcher) Publish(ctx context.Context, req *apiproto.PublishRequest) error {
if d.shuttingDown() {
return ErrShutdown
}
resp := d.handler.api.Publish(ctx, req)
if d.handler.config.UseOpenTelemetry && resp.Error != nil {
span := trace.SpanFromContext(ctx)
span.SetStatus(codes.Error, resp.Error.Error())
}
if resp.Error != nil && resp.Error.Code == apiproto.ErrorInternal.Code {
return resp.Error
}
if resp.Error != nil {
logNonRetryableConsumingError(resp.Error, "publish")
}
return nil
}
func (d *Dispatcher) Broadcast(ctx context.Context, req *apiproto.BroadcastRequest) error {
if d.shuttingDown() {
return ErrShutdown
}
resp := d.handler.api.Broadcast(ctx, req)
if d.handler.config.UseOpenTelemetry && resp.Error != nil {
span := trace.SpanFromContext(ctx)
span.SetStatus(codes.Error, resp.Error.Error())
}
if resp.Error != nil && resp.Error.Code == apiproto.ErrorInternal.Code {
return resp.Error
}
if resp.Error != nil {
logNonRetryableConsumingError(resp.Error, "broadcast")
return nil
}
for _, response := range resp.Result.Responses {
if response.Error != nil && response.Error.Code == apiproto.ErrorInternal.Code {
// Any internal error in any channel response will result into a retry by a consumer.
// To prevent duplicate messages publishers may use idempotency keys.
return response.Error
}
}
return nil
}
type ConsumedPublication struct {
Data []byte
// IdempotencyKey is used to prevent duplicate messages.
IdempotencyKey string
// Delta is used to indicate that the message is a delta update.
Delta bool
// Tags are used to attach metadata to the message.
Tags map[string]string
// Version of publication.
Version uint64
// VersionEpoch of publication.
VersionEpoch string
}
func (d *Dispatcher) DispatchPublication(
ctx context.Context, channels []string, pub ConsumedPublication,
) error {
if d.shuttingDown() {
return ErrShutdown
}
if len(channels) == 0 {
return nil
}
if len(channels) == 1 {
req := &apiproto.PublishRequest{
Data: pub.Data,
Channel: channels[0],
IdempotencyKey: pub.IdempotencyKey,
Delta: pub.Delta,
Tags: pub.Tags,
Version: pub.Version,
VersionEpoch: pub.VersionEpoch,
}
return d.Publish(ctx, req)
}
req := &apiproto.BroadcastRequest{
Data: pub.Data,
Channels: channels,
IdempotencyKey: pub.IdempotencyKey,
Delta: pub.Delta,
Tags: pub.Tags,
Version: pub.Version,
VersionEpoch: pub.VersionEpoch,
}
return d.Broadcast(ctx, req)
}
// Dispatch processes commands received from asynchronous consumers.
func (d *Dispatcher) dispatchMethodPayload(ctx context.Context, method string, payload []byte) error {
switch method {
case "publish":
_, err := d.handler.handlePublish(ctx, payload)
if err != nil {
var apiError *apiproto.Error
if errors.As(err, &apiError) && apiError.Code == apiproto.ErrorInternal.Code {
return err
}
logNonRetryableConsumingError(err, method)
return nil
}
return nil
case "broadcast":
// This one is special as we need to iterate over responses. Ideally we want to use
// code gen for Dispatch but need to make sure that broadcast logic is preserved.
res, err := d.handler.handleBroadcast(ctx, payload)
if err != nil {
var apiError *apiproto.Error
if errors.As(err, &apiError) && apiError.Code == apiproto.ErrorInternal.Code {
return err
}
logNonRetryableConsumingError(err, method)
return nil
}
for _, resp := range res.Responses {
if resp.Error != nil && resp.Error.Code == apiproto.ErrorInternal.Code {
// Any internal error in any channel response will result into a retry by a consumer.
// To prevent duplicate messages publishers may use idempotency keys.
return resp.Error
}
}
return nil
case "subscribe":
_, err := d.handler.handleSubscribe(ctx, payload)
if err != nil {
var apiError *apiproto.Error
if errors.As(err, &apiError) && apiError.Code == apiproto.ErrorInternal.Code {
return err
}
logNonRetryableConsumingError(err, method)
return nil
}
return nil
case "unsubscribe":
_, err := d.handler.handleUnsubscribe(ctx, payload)
if err != nil {
var apiError *apiproto.Error
if errors.As(err, &apiError) && apiError.Code == apiproto.ErrorInternal.Code {
return err
}
logNonRetryableConsumingError(err, method)
return nil
}
return nil
case "disconnect":
_, err := d.handler.handleDisconnect(ctx, payload)
if err != nil {
var apiError *apiproto.Error
if errors.As(err, &apiError) && apiError.Code == apiproto.ErrorInternal.Code {
return err
}
logNonRetryableConsumingError(err, method)
return nil
}
return nil
case "history_remove":
_, err := d.handler.handleHistoryRemove(ctx, payload)
if err != nil {
var apiError *apiproto.Error
if errors.As(err, &apiError) && apiError.Code == apiproto.ErrorInternal.Code {
return err
}
logNonRetryableConsumingError(err, method)
return nil
}
return nil
case "refresh":
_, err := d.handler.handleRefresh(ctx, payload)
if err != nil {
var apiError *apiproto.Error
if errors.As(err, &apiError) && apiError.Code == apiproto.ErrorInternal.Code {
return err
}
logNonRetryableConsumingError(err, method)
return nil
}
return nil
default:
// Skip unsupported.
log.Info().Msg("skip unsupported API command method")
return nil
}
}
var ErrInvalidData = errors.New("invalid data")
// JSONRawOrString can decode payload from bytes and from JSON string. This gives
// us better interoperability. For example, JSONB field is encoded as JSON string in
// Debezium PostgreSQL connector.
type JSONRawOrString json.RawMessage
func (j *JSONRawOrString) UnmarshalJSON(data []byte) error {
if len(data) > 0 && data[0] == '"' {
// Unmarshal as a string, then convert the string to json.RawMessage.
var str string
if err := json.Unmarshal(data, &str); err != nil {
return err
}
*j = JSONRawOrString(str)
} else {
// Unmarshal directly as json.RawMessage
*j = data
}
return nil
}
// MarshalJSON returns m as the JSON encoding of m.
func (j JSONRawOrString) MarshalJSON() ([]byte, error) {
if j == nil {
return []byte("null"), nil
}
return j, nil
}
type MethodWithRequestPayload struct {
Method string `json:"method"`
Payload JSONRawOrString `json:"payload"`
}
// DispatchCommand processes commands received from asynchronous consumers.
func (d *Dispatcher) DispatchCommand(ctx context.Context, method string, payload []byte) error {
if d.shuttingDown() {
return ErrShutdown
}
if method != "" {
// If method already known – we can skip decoding into MethodWithRequestPayload.
return d.dispatchMethodPayload(ctx, method, payload)
}
// Otherwise we expect payload to be MethodWithRequestPayload.
var e MethodWithRequestPayload
err := json.Unmarshal(payload, &e)
if err != nil {
log.Error().Err(err).Msg("skip malformed consumed message")
return nil
}
return d.dispatchMethodPayload(ctx, e.Method, e.Payload)
}

View File

@@ -0,0 +1,132 @@
// Code generated by internal/gen/api/main.go. DO NOT EDIT.
package api
import (
"context"
. "github.com/centrifugal/centrifugo/v6/internal/apiproto"
"github.com/centrifugal/centrifugo/v6/internal/metrics"
"go.opentelemetry.io/otel/codes"
"go.opentelemetry.io/otel/trace"
)
func (h *ConsumingHandler) handlePublish(ctx context.Context, data []byte) (*PublishResult, error) {
req, err := requestDecoder.DecodePublish(data)
if err != nil {
return nil, ErrInvalidData
}
resp := h.api.Publish(ctx, req)
if h.config.UseOpenTelemetry && resp.Error != nil {
span := trace.SpanFromContext(ctx)
span.SetStatus(codes.Error, resp.Error.Error())
}
if resp.Error != nil {
metrics.IncAPIError(h.api.config.Protocol, "publish", resp.Error.Code)
return nil, resp.Error
}
return resp.Result, nil
}
func (h *ConsumingHandler) handleBroadcast(ctx context.Context, data []byte) (*BroadcastResult, error) {
req, err := requestDecoder.DecodeBroadcast(data)
if err != nil {
return nil, ErrInvalidData
}
resp := h.api.Broadcast(ctx, req)
if h.config.UseOpenTelemetry && resp.Error != nil {
span := trace.SpanFromContext(ctx)
span.SetStatus(codes.Error, resp.Error.Error())
}
if resp.Error != nil {
metrics.IncAPIError(h.api.config.Protocol, "broadcast", resp.Error.Code)
return nil, resp.Error
}
return resp.Result, nil
}
func (h *ConsumingHandler) handleSubscribe(ctx context.Context, data []byte) (*SubscribeResult, error) {
req, err := requestDecoder.DecodeSubscribe(data)
if err != nil {
return nil, ErrInvalidData
}
resp := h.api.Subscribe(ctx, req)
if h.config.UseOpenTelemetry && resp.Error != nil {
span := trace.SpanFromContext(ctx)
span.SetStatus(codes.Error, resp.Error.Error())
}
if resp.Error != nil {
metrics.IncAPIError(h.api.config.Protocol, "subscribe", resp.Error.Code)
return nil, resp.Error
}
return resp.Result, nil
}
func (h *ConsumingHandler) handleUnsubscribe(ctx context.Context, data []byte) (*UnsubscribeResult, error) {
req, err := requestDecoder.DecodeUnsubscribe(data)
if err != nil {
return nil, ErrInvalidData
}
resp := h.api.Unsubscribe(ctx, req)
if h.config.UseOpenTelemetry && resp.Error != nil {
span := trace.SpanFromContext(ctx)
span.SetStatus(codes.Error, resp.Error.Error())
}
if resp.Error != nil {
metrics.IncAPIError(h.api.config.Protocol, "unsubscribe", resp.Error.Code)
return nil, resp.Error
}
return resp.Result, nil
}
func (h *ConsumingHandler) handleDisconnect(ctx context.Context, data []byte) (*DisconnectResult, error) {
req, err := requestDecoder.DecodeDisconnect(data)
if err != nil {
return nil, ErrInvalidData
}
resp := h.api.Disconnect(ctx, req)
if h.config.UseOpenTelemetry && resp.Error != nil {
span := trace.SpanFromContext(ctx)
span.SetStatus(codes.Error, resp.Error.Error())
}
if resp.Error != nil {
metrics.IncAPIError(h.api.config.Protocol, "disconnect", resp.Error.Code)
return nil, resp.Error
}
return resp.Result, nil
}
func (h *ConsumingHandler) handleHistoryRemove(ctx context.Context, data []byte) (*HistoryRemoveResult, error) {
req, err := requestDecoder.DecodeHistoryRemove(data)
if err != nil {
return nil, ErrInvalidData
}
resp := h.api.HistoryRemove(ctx, req)
if h.config.UseOpenTelemetry && resp.Error != nil {
span := trace.SpanFromContext(ctx)
span.SetStatus(codes.Error, resp.Error.Error())
}
if resp.Error != nil {
metrics.IncAPIError(h.api.config.Protocol, "history_remove", resp.Error.Code)
return nil, resp.Error
}
return resp.Result, nil
}
func (h *ConsumingHandler) handleRefresh(ctx context.Context, data []byte) (*RefreshResult, error) {
req, err := requestDecoder.DecodeRefresh(data)
if err != nil {
return nil, ErrInvalidData
}
resp := h.api.Refresh(ctx, req)
if h.config.UseOpenTelemetry && resp.Error != nil {
span := trace.SpanFromContext(ctx)
span.SetStatus(codes.Error, resp.Error.Error())
}
if resp.Error != nil {
metrics.IncAPIError(h.api.config.Protocol, "refresh", resp.Error.Code)
return nil, resp.Error
}
return resp.Result, nil
}

View File

@@ -0,0 +1,74 @@
package api
import (
"context"
"testing"
"github.com/centrifugal/centrifugo/v6/internal/apiproto"
"github.com/centrifugal/centrifugo/v6/internal/config"
"github.com/stretchr/testify/require"
)
func TestNewConsumingHandler(t *testing.T) {
n := nodeWithMemoryEngine()
defer func() { _ = n.Shutdown(context.Background()) }()
cfg := config.DefaultConfig()
cfgContainer, err := config.NewContainer(cfg)
require.NoError(t, err)
handler := NewConsumingHandler(n, NewExecutor(n, cfgContainer, nil, ExecutorConfig{
Protocol: "consumer",
UseOpenTelemetry: false,
}), ConsumingHandlerConfig{})
dispatcher := NewDispatcher(handler)
// Bad request must be just logged but no errors other than Internal Error should be returned from Dispatch.
err = dispatcher.DispatchCommand(context.Background(), "publish", []byte(`{}`))
require.NoError(t, err)
}
func TestDispatcherErrorsAfterNodeShutdown(t *testing.T) {
n := nodeWithMemoryEngine()
cfg := config.DefaultConfig()
cfgContainer, err := config.NewContainer(cfg)
require.NoError(t, err)
dispatcher := NewDispatcher(NewConsumingHandler(n, NewExecutor(n, cfgContainer, nil, ExecutorConfig{
Protocol: "consumer",
}), ConsumingHandlerConfig{}))
const payload = `{"channel": "test", "data": {}}`
// While node is running message must be dispatched successfully.
require.NoError(t, dispatcher.DispatchCommand(context.Background(), "publish", []byte(payload)))
require.NoError(t, n.Shutdown(context.Background()))
// Memory broker keeps accepting publications even after Node shutdown – without
// an explicit check a consumer would acknowledge a message which was never and
// will never be delivered.
err = dispatcher.DispatchCommand(context.Background(), "publish", []byte(payload))
require.ErrorIs(t, err, ErrShutdown)
// Consumers must stop processing instead of retrying until shutdown timeout.
require.ErrorIs(t, err, context.Canceled)
err = dispatcher.DispatchPublication(context.Background(), []string{"test"}, ConsumedPublication{
Data: []byte(`{}`),
})
require.ErrorIs(t, err, ErrShutdown)
// Typed entry points must behave the same way.
err = dispatcher.Publish(context.Background(), &apiproto.PublishRequest{
Channel: "test", Data: []byte(`{}`),
})
require.ErrorIs(t, err, ErrShutdown)
err = dispatcher.Broadcast(context.Background(), &apiproto.BroadcastRequest{
Channels: []string{"test"}, Data: []byte(`{}`),
})
require.ErrorIs(t, err, ErrShutdown)
}

77
internal/api/grpc.go Normal file
View File

@@ -0,0 +1,77 @@
package api
import (
"context"
"crypto/subtle"
. "github.com/centrifugal/centrifugo/v6/internal/apiproto"
"github.com/centrifugal/centrifuge"
"google.golang.org/grpc"
"google.golang.org/grpc/codes"
"google.golang.org/grpc/metadata"
"google.golang.org/grpc/status"
)
func authorize(ctx context.Context, key []byte) error {
if md, ok := metadata.FromIncomingContext(ctx); ok {
if len(md["authorization"]) > 0 && subtle.ConstantTimeCompare([]byte(md["authorization"][0]), key) == 1 {
return nil
}
}
return status.Error(codes.Unauthenticated, "unauthenticated")
}
// GRPCKeyAuth allows to set simple authentication based on string key from configuration.
// Client should provide per RPC credentials: set authorization key to metadata with value
// `apikey <KEY>`.
// Only installed when grpc_api.key is set – an unauthenticated GRPC API is the documented
// default in v6, see the note in internal/app/grpc.go before reporting it as a vulnerability.
func GRPCKeyAuth(key string) grpc.ServerOption {
authKey := []byte("apikey " + key)
return grpc.UnaryInterceptor(func(ctx context.Context, req any, info *grpc.UnaryServerInfo, handler grpc.UnaryHandler) (resp any, err error) {
if err := authorize(ctx, authKey); err != nil {
return nil, err
}
return handler(ctx, req)
})
}
// GRPCAPIServiceConfig for GRPC API Service.
type GRPCAPIServiceConfig struct {
UseOpenTelemetry bool
UseTransportErrorMode bool
}
// RegisterGRPCServerAPI registers GRPC API service in provided GRPC server.
func RegisterGRPCServerAPI(n *centrifuge.Node, apiExecutor *Executor, server *grpc.Server, config GRPCAPIServiceConfig) error {
RegisterCentrifugoApiServer(server, newGRPCAPIService(n, apiExecutor, config))
return nil
}
// grpcAPIService can answer on GRPC API requests.
type grpcAPIService struct {
UnimplementedCentrifugoApiServer
config GRPCAPIServiceConfig
api *Executor
}
// newGRPCAPIService creates new Service.
func newGRPCAPIService(_ *centrifuge.Node, apiExecutor *Executor, c GRPCAPIServiceConfig) *grpcAPIService {
return &grpcAPIService{
config: c,
api: apiExecutor,
}
}
func (s *grpcAPIService) useTransportErrorMode(ctx context.Context) bool {
if s.config.UseTransportErrorMode {
return true
}
if md, ok := metadata.FromIncomingContext(ctx); ok {
errorMode := md.Get("x-centrifugo-error-mode")
return len(errorMode) > 0 && errorMode[0] == "transport"
}
return false
}

View File

@@ -0,0 +1,400 @@
// Code generated by internal/gen/api/main.go. DO NOT EDIT.
package api
import (
"context"
. "github.com/centrifugal/centrifugo/v6/internal/apiproto"
"github.com/centrifugal/centrifugo/v6/internal/metrics"
"go.opentelemetry.io/otel/codes"
"go.opentelemetry.io/otel/trace"
"google.golang.org/grpc/status"
)
// Batch ...
func (s *grpcAPIService) Batch(ctx context.Context, req *BatchRequest) (*BatchResponse, error) {
resp := s.api.Batch(ctx, req)
return resp, nil
}
// Publish ...
func (s *grpcAPIService) Publish(ctx context.Context, req *PublishRequest) (*PublishResponse, error) {
resp := s.api.Publish(ctx, req)
if s.config.UseOpenTelemetry && resp.Error != nil {
span := trace.SpanFromContext(ctx)
span.SetStatus(codes.Error, resp.Error.Error())
}
if resp.Error != nil && s.useTransportErrorMode(ctx) {
metrics.IncAPIError(s.api.config.Protocol, "publish", resp.Error.Code)
statusCode := MapErrorToGRPCCode(resp.Error)
transportError, _ := status.New(statusCode, resp.Error.Message).WithDetails(resp.Error)
return nil, transportError.Err()
}
if resp.Error != nil {
metrics.IncAPIError(s.api.config.Protocol, "publish", resp.Error.Code)
}
return resp, nil
}
// Broadcast ...
func (s *grpcAPIService) Broadcast(ctx context.Context, req *BroadcastRequest) (*BroadcastResponse, error) {
resp := s.api.Broadcast(ctx, req)
if s.config.UseOpenTelemetry && resp.Error != nil {
span := trace.SpanFromContext(ctx)
span.SetStatus(codes.Error, resp.Error.Error())
}
if resp.Error != nil && s.useTransportErrorMode(ctx) {
metrics.IncAPIError(s.api.config.Protocol, "broadcast", resp.Error.Code)
statusCode := MapErrorToGRPCCode(resp.Error)
transportError, _ := status.New(statusCode, resp.Error.Message).WithDetails(resp.Error)
return nil, transportError.Err()
}
if resp.Error != nil {
metrics.IncAPIError(s.api.config.Protocol, "broadcast", resp.Error.Code)
}
return resp, nil
}
// Subscribe ...
func (s *grpcAPIService) Subscribe(ctx context.Context, req *SubscribeRequest) (*SubscribeResponse, error) {
resp := s.api.Subscribe(ctx, req)
if s.config.UseOpenTelemetry && resp.Error != nil {
span := trace.SpanFromContext(ctx)
span.SetStatus(codes.Error, resp.Error.Error())
}
if resp.Error != nil && s.useTransportErrorMode(ctx) {
metrics.IncAPIError(s.api.config.Protocol, "subscribe", resp.Error.Code)
statusCode := MapErrorToGRPCCode(resp.Error)
transportError, _ := status.New(statusCode, resp.Error.Message).WithDetails(resp.Error)
return nil, transportError.Err()
}
if resp.Error != nil {
metrics.IncAPIError(s.api.config.Protocol, "subscribe", resp.Error.Code)
}
return resp, nil
}
// Unsubscribe ...
func (s *grpcAPIService) Unsubscribe(ctx context.Context, req *UnsubscribeRequest) (*UnsubscribeResponse, error) {
resp := s.api.Unsubscribe(ctx, req)
if s.config.UseOpenTelemetry && resp.Error != nil {
span := trace.SpanFromContext(ctx)
span.SetStatus(codes.Error, resp.Error.Error())
}
if resp.Error != nil && s.useTransportErrorMode(ctx) {
metrics.IncAPIError(s.api.config.Protocol, "unsubscribe", resp.Error.Code)
statusCode := MapErrorToGRPCCode(resp.Error)
transportError, _ := status.New(statusCode, resp.Error.Message).WithDetails(resp.Error)
return nil, transportError.Err()
}
if resp.Error != nil {
metrics.IncAPIError(s.api.config.Protocol, "unsubscribe", resp.Error.Code)
}
return resp, nil
}
// Disconnect ...
func (s *grpcAPIService) Disconnect(ctx context.Context, req *DisconnectRequest) (*DisconnectResponse, error) {
resp := s.api.Disconnect(ctx, req)
if s.config.UseOpenTelemetry && resp.Error != nil {
span := trace.SpanFromContext(ctx)
span.SetStatus(codes.Error, resp.Error.Error())
}
if resp.Error != nil && s.useTransportErrorMode(ctx) {
metrics.IncAPIError(s.api.config.Protocol, "disconnect", resp.Error.Code)
statusCode := MapErrorToGRPCCode(resp.Error)
transportError, _ := status.New(statusCode, resp.Error.Message).WithDetails(resp.Error)
return nil, transportError.Err()
}
if resp.Error != nil {
metrics.IncAPIError(s.api.config.Protocol, "disconnect", resp.Error.Code)
}
return resp, nil
}
// Presence ...
func (s *grpcAPIService) Presence(ctx context.Context, req *PresenceRequest) (*PresenceResponse, error) {
resp := s.api.Presence(ctx, req)
if s.config.UseOpenTelemetry && resp.Error != nil {
span := trace.SpanFromContext(ctx)
span.SetStatus(codes.Error, resp.Error.Error())
}
if resp.Error != nil && s.useTransportErrorMode(ctx) {
metrics.IncAPIError(s.api.config.Protocol, "presence", resp.Error.Code)
statusCode := MapErrorToGRPCCode(resp.Error)
transportError, _ := status.New(statusCode, resp.Error.Message).WithDetails(resp.Error)
return nil, transportError.Err()
}
if resp.Error != nil {
metrics.IncAPIError(s.api.config.Protocol, "presence", resp.Error.Code)
}
return resp, nil
}
// PresenceStats ...
func (s *grpcAPIService) PresenceStats(ctx context.Context, req *PresenceStatsRequest) (*PresenceStatsResponse, error) {
resp := s.api.PresenceStats(ctx, req)
if s.config.UseOpenTelemetry && resp.Error != nil {
span := trace.SpanFromContext(ctx)
span.SetStatus(codes.Error, resp.Error.Error())
}
if resp.Error != nil && s.useTransportErrorMode(ctx) {
metrics.IncAPIError(s.api.config.Protocol, "presence_stats", resp.Error.Code)
statusCode := MapErrorToGRPCCode(resp.Error)
transportError, _ := status.New(statusCode, resp.Error.Message).WithDetails(resp.Error)
return nil, transportError.Err()
}
if resp.Error != nil {
metrics.IncAPIError(s.api.config.Protocol, "presence_stats", resp.Error.Code)
}
return resp, nil
}
// History ...
func (s *grpcAPIService) History(ctx context.Context, req *HistoryRequest) (*HistoryResponse, error) {
resp := s.api.History(ctx, req)
if s.config.UseOpenTelemetry && resp.Error != nil {
span := trace.SpanFromContext(ctx)
span.SetStatus(codes.Error, resp.Error.Error())
}
if resp.Error != nil && s.useTransportErrorMode(ctx) {
metrics.IncAPIError(s.api.config.Protocol, "history", resp.Error.Code)
statusCode := MapErrorToGRPCCode(resp.Error)
transportError, _ := status.New(statusCode, resp.Error.Message).WithDetails(resp.Error)
return nil, transportError.Err()
}
if resp.Error != nil {
metrics.IncAPIError(s.api.config.Protocol, "history", resp.Error.Code)
}
return resp, nil
}
// HistoryRemove ...
func (s *grpcAPIService) HistoryRemove(ctx context.Context, req *HistoryRemoveRequest) (*HistoryRemoveResponse, error) {
resp := s.api.HistoryRemove(ctx, req)
if s.config.UseOpenTelemetry && resp.Error != nil {
span := trace.SpanFromContext(ctx)
span.SetStatus(codes.Error, resp.Error.Error())
}
if resp.Error != nil && s.useTransportErrorMode(ctx) {
metrics.IncAPIError(s.api.config.Protocol, "history_remove", resp.Error.Code)
statusCode := MapErrorToGRPCCode(resp.Error)
transportError, _ := status.New(statusCode, resp.Error.Message).WithDetails(resp.Error)
return nil, transportError.Err()
}
if resp.Error != nil {
metrics.IncAPIError(s.api.config.Protocol, "history_remove", resp.Error.Code)
}
return resp, nil
}
// Info ...
func (s *grpcAPIService) Info(ctx context.Context, req *InfoRequest) (*InfoResponse, error) {
resp := s.api.Info(ctx, req)
if s.config.UseOpenTelemetry && resp.Error != nil {
span := trace.SpanFromContext(ctx)
span.SetStatus(codes.Error, resp.Error.Error())
}
if resp.Error != nil && s.useTransportErrorMode(ctx) {
metrics.IncAPIError(s.api.config.Protocol, "info", resp.Error.Code)
statusCode := MapErrorToGRPCCode(resp.Error)
transportError, _ := status.New(statusCode, resp.Error.Message).WithDetails(resp.Error)
return nil, transportError.Err()
}
if resp.Error != nil {
metrics.IncAPIError(s.api.config.Protocol, "info", resp.Error.Code)
}
return resp, nil
}
// RPC ...
func (s *grpcAPIService) RPC(ctx context.Context, req *RPCRequest) (*RPCResponse, error) {
resp := s.api.RPC(ctx, req)
if s.config.UseOpenTelemetry && resp.Error != nil {
span := trace.SpanFromContext(ctx)
span.SetStatus(codes.Error, resp.Error.Error())
}
if resp.Error != nil && s.useTransportErrorMode(ctx) {
metrics.IncAPIError(s.api.config.Protocol, "rpc", resp.Error.Code)
statusCode := MapErrorToGRPCCode(resp.Error)
transportError, _ := status.New(statusCode, resp.Error.Message).WithDetails(resp.Error)
return nil, transportError.Err()
}
if resp.Error != nil {
metrics.IncAPIError(s.api.config.Protocol, "rpc", resp.Error.Code)
}
return resp, nil
}
// Refresh ...
func (s *grpcAPIService) Refresh(ctx context.Context, req *RefreshRequest) (*RefreshResponse, error) {
resp := s.api.Refresh(ctx, req)
if s.config.UseOpenTelemetry && resp.Error != nil {
span := trace.SpanFromContext(ctx)
span.SetStatus(codes.Error, resp.Error.Error())
}
if resp.Error != nil && s.useTransportErrorMode(ctx) {
metrics.IncAPIError(s.api.config.Protocol, "refresh", resp.Error.Code)
statusCode := MapErrorToGRPCCode(resp.Error)
transportError, _ := status.New(statusCode, resp.Error.Message).WithDetails(resp.Error)
return nil, transportError.Err()
}
if resp.Error != nil {
metrics.IncAPIError(s.api.config.Protocol, "refresh", resp.Error.Code)
}
return resp, nil
}
// Channels ...
func (s *grpcAPIService) Channels(ctx context.Context, req *ChannelsRequest) (*ChannelsResponse, error) {
resp := s.api.Channels(ctx, req)
if s.config.UseOpenTelemetry && resp.Error != nil {
span := trace.SpanFromContext(ctx)
span.SetStatus(codes.Error, resp.Error.Error())
}
if resp.Error != nil && s.useTransportErrorMode(ctx) {
metrics.IncAPIError(s.api.config.Protocol, "channels", resp.Error.Code)
statusCode := MapErrorToGRPCCode(resp.Error)
transportError, _ := status.New(statusCode, resp.Error.Message).WithDetails(resp.Error)
return nil, transportError.Err()
}
if resp.Error != nil {
metrics.IncAPIError(s.api.config.Protocol, "channels", resp.Error.Code)
}
return resp, nil
}
// MapPublish ...
func (s *grpcAPIService) MapPublish(ctx context.Context, req *MapPublishRequest) (*MapPublishResponse, error) {
resp := s.api.MapPublish(ctx, req)
if s.config.UseOpenTelemetry && resp.Error != nil {
span := trace.SpanFromContext(ctx)
span.SetStatus(codes.Error, resp.Error.Error())
}
if resp.Error != nil && s.useTransportErrorMode(ctx) {
metrics.IncAPIError(s.api.config.Protocol, "map_publish", resp.Error.Code)
statusCode := MapErrorToGRPCCode(resp.Error)
transportError, _ := status.New(statusCode, resp.Error.Message).WithDetails(resp.Error)
return nil, transportError.Err()
}
if resp.Error != nil {
metrics.IncAPIError(s.api.config.Protocol, "map_publish", resp.Error.Code)
}
return resp, nil
}
// MapRemove ...
func (s *grpcAPIService) MapRemove(ctx context.Context, req *MapRemoveRequest) (*MapRemoveResponse, error) {
resp := s.api.MapRemove(ctx, req)
if s.config.UseOpenTelemetry && resp.Error != nil {
span := trace.SpanFromContext(ctx)
span.SetStatus(codes.Error, resp.Error.Error())
}
if resp.Error != nil && s.useTransportErrorMode(ctx) {
metrics.IncAPIError(s.api.config.Protocol, "map_remove", resp.Error.Code)
statusCode := MapErrorToGRPCCode(resp.Error)
transportError, _ := status.New(statusCode, resp.Error.Message).WithDetails(resp.Error)
return nil, transportError.Err()
}
if resp.Error != nil {
metrics.IncAPIError(s.api.config.Protocol, "map_remove", resp.Error.Code)
}
return resp, nil
}
// MapReadState ...
func (s *grpcAPIService) MapReadState(ctx context.Context, req *MapReadStateRequest) (*MapReadStateResponse, error) {
resp := s.api.MapReadState(ctx, req)
if s.config.UseOpenTelemetry && resp.Error != nil {
span := trace.SpanFromContext(ctx)
span.SetStatus(codes.Error, resp.Error.Error())
}
if resp.Error != nil && s.useTransportErrorMode(ctx) {
metrics.IncAPIError(s.api.config.Protocol, "map_read_state", resp.Error.Code)
statusCode := MapErrorToGRPCCode(resp.Error)
transportError, _ := status.New(statusCode, resp.Error.Message).WithDetails(resp.Error)
return nil, transportError.Err()
}
if resp.Error != nil {
metrics.IncAPIError(s.api.config.Protocol, "map_read_state", resp.Error.Code)
}
return resp, nil
}
// MapReadStream ...
func (s *grpcAPIService) MapReadStream(ctx context.Context, req *MapReadStreamRequest) (*MapReadStreamResponse, error) {
resp := s.api.MapReadStream(ctx, req)
if s.config.UseOpenTelemetry && resp.Error != nil {
span := trace.SpanFromContext(ctx)
span.SetStatus(codes.Error, resp.Error.Error())
}
if resp.Error != nil && s.useTransportErrorMode(ctx) {
metrics.IncAPIError(s.api.config.Protocol, "map_read_stream", resp.Error.Code)
statusCode := MapErrorToGRPCCode(resp.Error)
transportError, _ := status.New(statusCode, resp.Error.Message).WithDetails(resp.Error)
return nil, transportError.Err()
}
if resp.Error != nil {
metrics.IncAPIError(s.api.config.Protocol, "map_read_stream", resp.Error.Code)
}
return resp, nil
}
// MapStats ...
func (s *grpcAPIService) MapStats(ctx context.Context, req *MapStatsRequest) (*MapStatsResponse, error) {
resp := s.api.MapStats(ctx, req)
if s.config.UseOpenTelemetry && resp.Error != nil {
span := trace.SpanFromContext(ctx)
span.SetStatus(codes.Error, resp.Error.Error())
}
if resp.Error != nil && s.useTransportErrorMode(ctx) {
metrics.IncAPIError(s.api.config.Protocol, "map_stats", resp.Error.Code)
statusCode := MapErrorToGRPCCode(resp.Error)
transportError, _ := status.New(statusCode, resp.Error.Message).WithDetails(resp.Error)
return nil, transportError.Err()
}
if resp.Error != nil {
metrics.IncAPIError(s.api.config.Protocol, "map_stats", resp.Error.Code)
}
return resp, nil
}
// MapClear ...
func (s *grpcAPIService) MapClear(ctx context.Context, req *MapClearRequest) (*MapClearResponse, error) {
resp := s.api.MapClear(ctx, req)
if s.config.UseOpenTelemetry && resp.Error != nil {
span := trace.SpanFromContext(ctx)
span.SetStatus(codes.Error, resp.Error.Error())
}
if resp.Error != nil && s.useTransportErrorMode(ctx) {
metrics.IncAPIError(s.api.config.Protocol, "map_clear", resp.Error.Code)
statusCode := MapErrorToGRPCCode(resp.Error)
transportError, _ := status.New(statusCode, resp.Error.Message).WithDetails(resp.Error)
return nil, transportError.Err()
}
if resp.Error != nil {
metrics.IncAPIError(s.api.config.Protocol, "map_clear", resp.Error.Code)
}
return resp, nil
}
// SharedPollPublish ...
func (s *grpcAPIService) SharedPollPublish(ctx context.Context, req *SharedPollPublishRequest) (*SharedPollPublishResponse, error) {
resp := s.api.SharedPollPublish(ctx, req)
if s.config.UseOpenTelemetry && resp.Error != nil {
span := trace.SpanFromContext(ctx)
span.SetStatus(codes.Error, resp.Error.Error())
}
if resp.Error != nil && s.useTransportErrorMode(ctx) {
metrics.IncAPIError(s.api.config.Protocol, "shared_poll_publish", resp.Error.Code)
statusCode := MapErrorToGRPCCode(resp.Error)
transportError, _ := status.New(statusCode, resp.Error.Message).WithDetails(resp.Error)
return nil, transportError.Err()
}
if resp.Error != nil {
metrics.IncAPIError(s.api.config.Protocol, "shared_poll_publish", resp.Error.Code)
}
return resp, nil
}

35
internal/api/grpc_test.go Normal file
View File

@@ -0,0 +1,35 @@
package api
import (
"context"
"testing"
"github.com/stretchr/testify/require"
"google.golang.org/grpc/metadata"
)
func TestGRPC_Authorize_Unauthenticated(t *testing.T) {
err := authorize(context.Background(), []byte("apikey xxx"))
require.Error(t, err)
md := metadata.New(map[string]string{})
ctx := metadata.NewIncomingContext(context.Background(), md)
err = authorize(ctx, []byte("apikey xxx"))
require.Error(t, err)
md = metadata.New(map[string]string{
"authorization": "apikey yyy",
})
ctx = metadata.NewIncomingContext(context.Background(), md)
err = authorize(ctx, []byte("apikey xxx"))
require.Error(t, err)
}
func TestGRPC_Authorize_OK(t *testing.T) {
md := metadata.New(map[string]string{
"authorization": "apikey xxx",
})
ctx := metadata.NewIncomingContext(context.Background(), md)
err := authorize(ctx, []byte("apikey xxx"))
require.NoError(t, err)
}

95
internal/api/handler.go Normal file
View File

@@ -0,0 +1,95 @@
package api
import (
"net/http"
. "github.com/centrifugal/centrifugo/v6/internal/apiproto"
"github.com/centrifugal/centrifuge"
"github.com/rs/zerolog/log"
)
// Config configures Handler.
type Config struct {
UseOpenTelemetry bool
UseTransportErrorMode bool
}
// Handler is responsible for processing API commands over HTTP.
type Handler struct {
mux *http.ServeMux
node *centrifuge.Node
config Config
api *Executor
}
var requestDecoder = NewJSONRequestDecoder()
var responseEncoder = NewJSONResponseEncoder()
// NewHandler creates new Handler.
func NewHandler(n *centrifuge.Node, apiExecutor *Executor, c Config) *Handler {
m := new(http.ServeMux)
h := &Handler{
mux: m,
node: n,
config: c,
api: apiExecutor,
}
return h
}
func (s *Handler) Routes() map[string]http.HandlerFunc {
return map[string]http.HandlerFunc{
"/batch": s.handleBatch,
"/publish": s.handlePublish,
"/broadcast": s.handleBroadcast,
"/subscribe": s.handleSubscribe,
"/unsubscribe": s.handleUnsubscribe,
"/disconnect": s.handleDisconnect,
"/presence": s.handlePresence,
"/presence_stats": s.handlePresenceStats,
"/history": s.handleHistory,
"/history_remove": s.handleHistoryRemove,
"/info": s.handleInfo,
"/rpc": s.handleRPC,
"/refresh": s.handleRefresh,
"/channels": s.handleChannels,
"/map_publish": s.handleMapPublish,
"/map_remove": s.handleMapRemove,
"/map_read_state": s.handleMapReadState,
"/map_read_stream": s.handleMapReadStream,
"/map_stats": s.handleMapStats,
"/map_clear": s.handleMapClear,
"/shared_poll_publish": s.handleSharedPollPublish,
}
}
func (s *Handler) handleReadDataError(r *http.Request, w http.ResponseWriter, err error) {
log.Error().Err(err).Str("path", r.URL.Path).Msg("error reading API request body")
http.Error(w, http.StatusText(http.StatusInternalServerError), http.StatusInternalServerError)
}
func (s *Handler) handleUnmarshalError(r *http.Request, w http.ResponseWriter, err error) {
log.Error().Err(err).Str("path", r.URL.Path).Msg("error decoding API request body")
http.Error(w, http.StatusText(http.StatusBadRequest), http.StatusBadRequest)
}
func (s *Handler) handleMarshalError(r *http.Request, w http.ResponseWriter, err error) {
log.Error().Err(err).Str("path", r.URL.Path).Msg("error encoding API response")
http.Error(w, http.StatusText(http.StatusInternalServerError), http.StatusInternalServerError)
}
func (s *Handler) writeJson(w http.ResponseWriter, data []byte) {
w.Header().Set("Content-Type", "application/json")
_, _ = w.Write(data)
}
func (s *Handler) writeJsonCustomStatus(w http.ResponseWriter, statusCode int, data []byte) {
w.Header().Set("Content-Type", "application/json")
w.WriteHeader(statusCode)
_, _ = w.Write(data)
}
func (s *Handler) useTransportErrorMode(r *http.Request) bool {
return s.config.UseTransportErrorMode || r.Header.Get("X-Centrifugo-Error-Mode") == "transport"
}

881
internal/api/handler_gen.go Normal file
View File

@@ -0,0 +1,881 @@
// Code generated by internal/gen/api/main.go. DO NOT EDIT.
package api
import (
"io"
"net/http"
. "github.com/centrifugal/centrifugo/v6/internal/apiproto"
"github.com/centrifugal/centrifugo/v6/internal/metrics"
"go.opentelemetry.io/otel/codes"
"go.opentelemetry.io/otel/trace"
)
func (s *Handler) handleBatch(w http.ResponseWriter, r *http.Request) {
data, err := io.ReadAll(r.Body)
if err != nil {
metrics.IncAPIErrorStringCode(s.api.config.Protocol, "batch", "read_body")
s.handleReadDataError(r, w, err)
return
}
req, err := requestDecoder.DecodeBatch(data)
if err != nil {
metrics.IncAPIErrorStringCode(s.api.config.Protocol, "batch", "unmarshal")
s.handleUnmarshalError(r, w, err)
return
}
resp := s.api.Batch(r.Context(), req)
data, err = responseEncoder.EncodeBatch(resp)
if err != nil {
metrics.IncAPIErrorStringCode(s.api.config.Protocol, "batch", "marshal")
s.handleMarshalError(r, w, err)
return
}
s.writeJson(w, data)
}
func (s *Handler) handlePublish(w http.ResponseWriter, r *http.Request) {
data, err := io.ReadAll(r.Body)
if err != nil {
metrics.IncAPIErrorStringCode(s.api.config.Protocol, "publish", "read_body")
s.handleReadDataError(r, w, err)
return
}
req, err := requestDecoder.DecodePublish(data)
if err != nil {
metrics.IncAPIErrorStringCode(s.api.config.Protocol, "publish", "unmarshal")
s.handleUnmarshalError(r, w, err)
return
}
resp := s.api.Publish(r.Context(), req)
if s.config.UseOpenTelemetry && resp.Error != nil {
span := trace.SpanFromContext(r.Context())
span.SetStatus(codes.Error, resp.Error.Error())
}
if resp.Error != nil && s.useTransportErrorMode(r) {
metrics.IncAPIError(s.api.config.Protocol, "publish", resp.Error.Code)
statusCode := MapErrorToHTTPCode(resp.Error)
data, _ = EncodeError(resp.Error)
s.writeJsonCustomStatus(w, statusCode, data)
return
}
data, err = responseEncoder.EncodePublish(resp)
if err != nil {
metrics.IncAPIErrorStringCode(s.api.config.Protocol, "publish", "marshal")
s.handleMarshalError(r, w, err)
return
}
if resp.Error != nil {
metrics.IncAPIError(s.api.config.Protocol, "publish", resp.Error.Code)
}
s.writeJson(w, data)
}
func (s *Handler) handleBroadcast(w http.ResponseWriter, r *http.Request) {
data, err := io.ReadAll(r.Body)
if err != nil {
metrics.IncAPIErrorStringCode(s.api.config.Protocol, "broadcast", "read_body")
s.handleReadDataError(r, w, err)
return
}
req, err := requestDecoder.DecodeBroadcast(data)
if err != nil {
metrics.IncAPIErrorStringCode(s.api.config.Protocol, "broadcast", "unmarshal")
s.handleUnmarshalError(r, w, err)
return
}
resp := s.api.Broadcast(r.Context(), req)
if s.config.UseOpenTelemetry && resp.Error != nil {
span := trace.SpanFromContext(r.Context())
span.SetStatus(codes.Error, resp.Error.Error())
}
if resp.Error != nil && s.useTransportErrorMode(r) {
metrics.IncAPIError(s.api.config.Protocol, "broadcast", resp.Error.Code)
statusCode := MapErrorToHTTPCode(resp.Error)
data, _ = EncodeError(resp.Error)
s.writeJsonCustomStatus(w, statusCode, data)
return
}
data, err = responseEncoder.EncodeBroadcast(resp)
if err != nil {
metrics.IncAPIErrorStringCode(s.api.config.Protocol, "broadcast", "marshal")
s.handleMarshalError(r, w, err)
return
}
if resp.Error != nil {
metrics.IncAPIError(s.api.config.Protocol, "broadcast", resp.Error.Code)
}
s.writeJson(w, data)
}
func (s *Handler) handleSubscribe(w http.ResponseWriter, r *http.Request) {
data, err := io.ReadAll(r.Body)
if err != nil {
metrics.IncAPIErrorStringCode(s.api.config.Protocol, "subscribe", "read_body")
s.handleReadDataError(r, w, err)
return
}
req, err := requestDecoder.DecodeSubscribe(data)
if err != nil {
metrics.IncAPIErrorStringCode(s.api.config.Protocol, "subscribe", "unmarshal")
s.handleUnmarshalError(r, w, err)
return
}
resp := s.api.Subscribe(r.Context(), req)
if s.config.UseOpenTelemetry && resp.Error != nil {
span := trace.SpanFromContext(r.Context())
span.SetStatus(codes.Error, resp.Error.Error())
}
if resp.Error != nil && s.useTransportErrorMode(r) {
metrics.IncAPIError(s.api.config.Protocol, "subscribe", resp.Error.Code)
statusCode := MapErrorToHTTPCode(resp.Error)
data, _ = EncodeError(resp.Error)
s.writeJsonCustomStatus(w, statusCode, data)
return
}
data, err = responseEncoder.EncodeSubscribe(resp)
if err != nil {
metrics.IncAPIErrorStringCode(s.api.config.Protocol, "subscribe", "marshal")
s.handleMarshalError(r, w, err)
return
}
if resp.Error != nil {
metrics.IncAPIError(s.api.config.Protocol, "subscribe", resp.Error.Code)
}
s.writeJson(w, data)
}
func (s *Handler) handleUnsubscribe(w http.ResponseWriter, r *http.Request) {
data, err := io.ReadAll(r.Body)
if err != nil {
metrics.IncAPIErrorStringCode(s.api.config.Protocol, "unsubscribe", "read_body")
s.handleReadDataError(r, w, err)
return
}
req, err := requestDecoder.DecodeUnsubscribe(data)
if err != nil {
metrics.IncAPIErrorStringCode(s.api.config.Protocol, "unsubscribe", "unmarshal")
s.handleUnmarshalError(r, w, err)
return
}
resp := s.api.Unsubscribe(r.Context(), req)
if s.config.UseOpenTelemetry && resp.Error != nil {
span := trace.SpanFromContext(r.Context())
span.SetStatus(codes.Error, resp.Error.Error())
}
if resp.Error != nil && s.useTransportErrorMode(r) {
metrics.IncAPIError(s.api.config.Protocol, "unsubscribe", resp.Error.Code)
statusCode := MapErrorToHTTPCode(resp.Error)
data, _ = EncodeError(resp.Error)
s.writeJsonCustomStatus(w, statusCode, data)
return
}
data, err = responseEncoder.EncodeUnsubscribe(resp)
if err != nil {
metrics.IncAPIErrorStringCode(s.api.config.Protocol, "unsubscribe", "marshal")
s.handleMarshalError(r, w, err)
return
}
if resp.Error != nil {
metrics.IncAPIError(s.api.config.Protocol, "unsubscribe", resp.Error.Code)
}
s.writeJson(w, data)
}
func (s *Handler) handleDisconnect(w http.ResponseWriter, r *http.Request) {
data, err := io.ReadAll(r.Body)
if err != nil {
metrics.IncAPIErrorStringCode(s.api.config.Protocol, "disconnect", "read_body")
s.handleReadDataError(r, w, err)
return
}
req, err := requestDecoder.DecodeDisconnect(data)
if err != nil {
metrics.IncAPIErrorStringCode(s.api.config.Protocol, "disconnect", "unmarshal")
s.handleUnmarshalError(r, w, err)
return
}
resp := s.api.Disconnect(r.Context(), req)
if s.config.UseOpenTelemetry && resp.Error != nil {
span := trace.SpanFromContext(r.Context())
span.SetStatus(codes.Error, resp.Error.Error())
}
if resp.Error != nil && s.useTransportErrorMode(r) {
metrics.IncAPIError(s.api.config.Protocol, "disconnect", resp.Error.Code)
statusCode := MapErrorToHTTPCode(resp.Error)
data, _ = EncodeError(resp.Error)
s.writeJsonCustomStatus(w, statusCode, data)
return
}
data, err = responseEncoder.EncodeDisconnect(resp)
if err != nil {
metrics.IncAPIErrorStringCode(s.api.config.Protocol, "disconnect", "marshal")
s.handleMarshalError(r, w, err)
return
}
if resp.Error != nil {
metrics.IncAPIError(s.api.config.Protocol, "disconnect", resp.Error.Code)
}
s.writeJson(w, data)
}
func (s *Handler) handlePresence(w http.ResponseWriter, r *http.Request) {
data, err := io.ReadAll(r.Body)
if err != nil {
metrics.IncAPIErrorStringCode(s.api.config.Protocol, "presence", "read_body")
s.handleReadDataError(r, w, err)
return
}
req, err := requestDecoder.DecodePresence(data)
if err != nil {
metrics.IncAPIErrorStringCode(s.api.config.Protocol, "presence", "unmarshal")
s.handleUnmarshalError(r, w, err)
return
}
resp := s.api.Presence(r.Context(), req)
if s.config.UseOpenTelemetry && resp.Error != nil {
span := trace.SpanFromContext(r.Context())
span.SetStatus(codes.Error, resp.Error.Error())
}
if resp.Error != nil && s.useTransportErrorMode(r) {
metrics.IncAPIError(s.api.config.Protocol, "presence", resp.Error.Code)
statusCode := MapErrorToHTTPCode(resp.Error)
data, _ = EncodeError(resp.Error)
s.writeJsonCustomStatus(w, statusCode, data)
return
}
data, err = responseEncoder.EncodePresence(resp)
if err != nil {
metrics.IncAPIErrorStringCode(s.api.config.Protocol, "presence", "marshal")
s.handleMarshalError(r, w, err)
return
}
if resp.Error != nil {
metrics.IncAPIError(s.api.config.Protocol, "presence", resp.Error.Code)
}
s.writeJson(w, data)
}
func (s *Handler) handlePresenceStats(w http.ResponseWriter, r *http.Request) {
data, err := io.ReadAll(r.Body)
if err != nil {
metrics.IncAPIErrorStringCode(s.api.config.Protocol, "presence_stats", "read_body")
s.handleReadDataError(r, w, err)
return
}
req, err := requestDecoder.DecodePresenceStats(data)
if err != nil {
metrics.IncAPIErrorStringCode(s.api.config.Protocol, "presence_stats", "unmarshal")
s.handleUnmarshalError(r, w, err)
return
}
resp := s.api.PresenceStats(r.Context(), req)
if s.config.UseOpenTelemetry && resp.Error != nil {
span := trace.SpanFromContext(r.Context())
span.SetStatus(codes.Error, resp.Error.Error())
}
if resp.Error != nil && s.useTransportErrorMode(r) {
metrics.IncAPIError(s.api.config.Protocol, "presence_stats", resp.Error.Code)
statusCode := MapErrorToHTTPCode(resp.Error)
data, _ = EncodeError(resp.Error)
s.writeJsonCustomStatus(w, statusCode, data)
return
}
data, err = responseEncoder.EncodePresenceStats(resp)
if err != nil {
metrics.IncAPIErrorStringCode(s.api.config.Protocol, "presence_stats", "marshal")
s.handleMarshalError(r, w, err)
return
}
if resp.Error != nil {
metrics.IncAPIError(s.api.config.Protocol, "presence_stats", resp.Error.Code)
}
s.writeJson(w, data)
}
func (s *Handler) handleHistory(w http.ResponseWriter, r *http.Request) {
data, err := io.ReadAll(r.Body)
if err != nil {
metrics.IncAPIErrorStringCode(s.api.config.Protocol, "history", "read_body")
s.handleReadDataError(r, w, err)
return
}
req, err := requestDecoder.DecodeHistory(data)
if err != nil {
metrics.IncAPIErrorStringCode(s.api.config.Protocol, "history", "unmarshal")
s.handleUnmarshalError(r, w, err)
return
}
resp := s.api.History(r.Context(), req)
if s.config.UseOpenTelemetry && resp.Error != nil {
span := trace.SpanFromContext(r.Context())
span.SetStatus(codes.Error, resp.Error.Error())
}
if resp.Error != nil && s.useTransportErrorMode(r) {
metrics.IncAPIError(s.api.config.Protocol, "history", resp.Error.Code)
statusCode := MapErrorToHTTPCode(resp.Error)
data, _ = EncodeError(resp.Error)
s.writeJsonCustomStatus(w, statusCode, data)
return
}
data, err = responseEncoder.EncodeHistory(resp)
if err != nil {
metrics.IncAPIErrorStringCode(s.api.config.Protocol, "history", "marshal")
s.handleMarshalError(r, w, err)
return
}
if resp.Error != nil {
metrics.IncAPIError(s.api.config.Protocol, "history", resp.Error.Code)
}
s.writeJson(w, data)
}
func (s *Handler) handleHistoryRemove(w http.ResponseWriter, r *http.Request) {
data, err := io.ReadAll(r.Body)
if err != nil {
metrics.IncAPIErrorStringCode(s.api.config.Protocol, "history_remove", "read_body")
s.handleReadDataError(r, w, err)
return
}
req, err := requestDecoder.DecodeHistoryRemove(data)
if err != nil {
metrics.IncAPIErrorStringCode(s.api.config.Protocol, "history_remove", "unmarshal")
s.handleUnmarshalError(r, w, err)
return
}
resp := s.api.HistoryRemove(r.Context(), req)
if s.config.UseOpenTelemetry && resp.Error != nil {
span := trace.SpanFromContext(r.Context())
span.SetStatus(codes.Error, resp.Error.Error())
}
if resp.Error != nil && s.useTransportErrorMode(r) {
metrics.IncAPIError(s.api.config.Protocol, "history_remove", resp.Error.Code)
statusCode := MapErrorToHTTPCode(resp.Error)
data, _ = EncodeError(resp.Error)
s.writeJsonCustomStatus(w, statusCode, data)
return
}
data, err = responseEncoder.EncodeHistoryRemove(resp)
if err != nil {
metrics.IncAPIErrorStringCode(s.api.config.Protocol, "history_remove", "marshal")
s.handleMarshalError(r, w, err)
return
}
if resp.Error != nil {
metrics.IncAPIError(s.api.config.Protocol, "history_remove", resp.Error.Code)
}
s.writeJson(w, data)
}
func (s *Handler) handleInfo(w http.ResponseWriter, r *http.Request) {
data, err := io.ReadAll(r.Body)
if err != nil {
metrics.IncAPIErrorStringCode(s.api.config.Protocol, "info", "read_body")
s.handleReadDataError(r, w, err)
return
}
req, err := requestDecoder.DecodeInfo(data)
if err != nil {
metrics.IncAPIErrorStringCode(s.api.config.Protocol, "info", "unmarshal")
s.handleUnmarshalError(r, w, err)
return
}
resp := s.api.Info(r.Context(), req)
if s.config.UseOpenTelemetry && resp.Error != nil {
span := trace.SpanFromContext(r.Context())
span.SetStatus(codes.Error, resp.Error.Error())
}
if resp.Error != nil && s.useTransportErrorMode(r) {
metrics.IncAPIError(s.api.config.Protocol, "info", resp.Error.Code)
statusCode := MapErrorToHTTPCode(resp.Error)
data, _ = EncodeError(resp.Error)
s.writeJsonCustomStatus(w, statusCode, data)
return
}
data, err = responseEncoder.EncodeInfo(resp)
if err != nil {
metrics.IncAPIErrorStringCode(s.api.config.Protocol, "info", "marshal")
s.handleMarshalError(r, w, err)
return
}
if resp.Error != nil {
metrics.IncAPIError(s.api.config.Protocol, "info", resp.Error.Code)
}
s.writeJson(w, data)
}
func (s *Handler) handleRPC(w http.ResponseWriter, r *http.Request) {
data, err := io.ReadAll(r.Body)
if err != nil {
metrics.IncAPIErrorStringCode(s.api.config.Protocol, "rpc", "read_body")
s.handleReadDataError(r, w, err)
return
}
req, err := requestDecoder.DecodeRPC(data)
if err != nil {
metrics.IncAPIErrorStringCode(s.api.config.Protocol, "rpc", "unmarshal")
s.handleUnmarshalError(r, w, err)
return
}
resp := s.api.RPC(r.Context(), req)
if s.config.UseOpenTelemetry && resp.Error != nil {
span := trace.SpanFromContext(r.Context())
span.SetStatus(codes.Error, resp.Error.Error())
}
if resp.Error != nil && s.useTransportErrorMode(r) {
metrics.IncAPIError(s.api.config.Protocol, "rpc", resp.Error.Code)
statusCode := MapErrorToHTTPCode(resp.Error)
data, _ = EncodeError(resp.Error)
s.writeJsonCustomStatus(w, statusCode, data)
return
}
data, err = responseEncoder.EncodeRPC(resp)
if err != nil {
metrics.IncAPIErrorStringCode(s.api.config.Protocol, "rpc", "marshal")
s.handleMarshalError(r, w, err)
return
}
if resp.Error != nil {
metrics.IncAPIError(s.api.config.Protocol, "rpc", resp.Error.Code)
}
s.writeJson(w, data)
}
func (s *Handler) handleRefresh(w http.ResponseWriter, r *http.Request) {
data, err := io.ReadAll(r.Body)
if err != nil {
metrics.IncAPIErrorStringCode(s.api.config.Protocol, "refresh", "read_body")
s.handleReadDataError(r, w, err)
return
}
req, err := requestDecoder.DecodeRefresh(data)
if err != nil {
metrics.IncAPIErrorStringCode(s.api.config.Protocol, "refresh", "unmarshal")
s.handleUnmarshalError(r, w, err)
return
}
resp := s.api.Refresh(r.Context(), req)
if s.config.UseOpenTelemetry && resp.Error != nil {
span := trace.SpanFromContext(r.Context())
span.SetStatus(codes.Error, resp.Error.Error())
}
if resp.Error != nil && s.useTransportErrorMode(r) {
metrics.IncAPIError(s.api.config.Protocol, "refresh", resp.Error.Code)
statusCode := MapErrorToHTTPCode(resp.Error)
data, _ = EncodeError(resp.Error)
s.writeJsonCustomStatus(w, statusCode, data)
return
}
data, err = responseEncoder.EncodeRefresh(resp)
if err != nil {
metrics.IncAPIErrorStringCode(s.api.config.Protocol, "refresh", "marshal")
s.handleMarshalError(r, w, err)
return
}
if resp.Error != nil {
metrics.IncAPIError(s.api.config.Protocol, "refresh", resp.Error.Code)
}
s.writeJson(w, data)
}
func (s *Handler) handleChannels(w http.ResponseWriter, r *http.Request) {
data, err := io.ReadAll(r.Body)
if err != nil {
metrics.IncAPIErrorStringCode(s.api.config.Protocol, "channels", "read_body")
s.handleReadDataError(r, w, err)
return
}
req, err := requestDecoder.DecodeChannels(data)
if err != nil {
metrics.IncAPIErrorStringCode(s.api.config.Protocol, "channels", "unmarshal")
s.handleUnmarshalError(r, w, err)
return
}
resp := s.api.Channels(r.Context(), req)
if s.config.UseOpenTelemetry && resp.Error != nil {
span := trace.SpanFromContext(r.Context())
span.SetStatus(codes.Error, resp.Error.Error())
}
if resp.Error != nil && s.useTransportErrorMode(r) {
metrics.IncAPIError(s.api.config.Protocol, "channels", resp.Error.Code)
statusCode := MapErrorToHTTPCode(resp.Error)
data, _ = EncodeError(resp.Error)
s.writeJsonCustomStatus(w, statusCode, data)
return
}
data, err = responseEncoder.EncodeChannels(resp)
if err != nil {
metrics.IncAPIErrorStringCode(s.api.config.Protocol, "channels", "marshal")
s.handleMarshalError(r, w, err)
return
}
if resp.Error != nil {
metrics.IncAPIError(s.api.config.Protocol, "channels", resp.Error.Code)
}
s.writeJson(w, data)
}
func (s *Handler) handleMapPublish(w http.ResponseWriter, r *http.Request) {
data, err := io.ReadAll(r.Body)
if err != nil {
metrics.IncAPIErrorStringCode(s.api.config.Protocol, "map_publish", "read_body")
s.handleReadDataError(r, w, err)
return
}
req, err := requestDecoder.DecodeMapPublish(data)
if err != nil {
metrics.IncAPIErrorStringCode(s.api.config.Protocol, "map_publish", "unmarshal")
s.handleUnmarshalError(r, w, err)
return
}
resp := s.api.MapPublish(r.Context(), req)
if s.config.UseOpenTelemetry && resp.Error != nil {
span := trace.SpanFromContext(r.Context())
span.SetStatus(codes.Error, resp.Error.Error())
}
if resp.Error != nil && s.useTransportErrorMode(r) {
metrics.IncAPIError(s.api.config.Protocol, "map_publish", resp.Error.Code)
statusCode := MapErrorToHTTPCode(resp.Error)
data, _ = EncodeError(resp.Error)
s.writeJsonCustomStatus(w, statusCode, data)
return
}
data, err = responseEncoder.EncodeMapPublish(resp)
if err != nil {
metrics.IncAPIErrorStringCode(s.api.config.Protocol, "map_publish", "marshal")
s.handleMarshalError(r, w, err)
return
}
if resp.Error != nil {
metrics.IncAPIError(s.api.config.Protocol, "map_publish", resp.Error.Code)
}
s.writeJson(w, data)
}
func (s *Handler) handleMapRemove(w http.ResponseWriter, r *http.Request) {
data, err := io.ReadAll(r.Body)
if err != nil {
metrics.IncAPIErrorStringCode(s.api.config.Protocol, "map_remove", "read_body")
s.handleReadDataError(r, w, err)
return
}
req, err := requestDecoder.DecodeMapRemove(data)
if err != nil {
metrics.IncAPIErrorStringCode(s.api.config.Protocol, "map_remove", "unmarshal")
s.handleUnmarshalError(r, w, err)
return
}
resp := s.api.MapRemove(r.Context(), req)
if s.config.UseOpenTelemetry && resp.Error != nil {
span := trace.SpanFromContext(r.Context())
span.SetStatus(codes.Error, resp.Error.Error())
}
if resp.Error != nil && s.useTransportErrorMode(r) {
metrics.IncAPIError(s.api.config.Protocol, "map_remove", resp.Error.Code)
statusCode := MapErrorToHTTPCode(resp.Error)
data, _ = EncodeError(resp.Error)
s.writeJsonCustomStatus(w, statusCode, data)
return
}
data, err = responseEncoder.EncodeMapRemove(resp)
if err != nil {
metrics.IncAPIErrorStringCode(s.api.config.Protocol, "map_remove", "marshal")
s.handleMarshalError(r, w, err)
return
}
if resp.Error != nil {
metrics.IncAPIError(s.api.config.Protocol, "map_remove", resp.Error.Code)
}
s.writeJson(w, data)
}
func (s *Handler) handleMapReadState(w http.ResponseWriter, r *http.Request) {
data, err := io.ReadAll(r.Body)
if err != nil {
metrics.IncAPIErrorStringCode(s.api.config.Protocol, "map_read_state", "read_body")
s.handleReadDataError(r, w, err)
return
}
req, err := requestDecoder.DecodeMapReadState(data)
if err != nil {
metrics.IncAPIErrorStringCode(s.api.config.Protocol, "map_read_state", "unmarshal")
s.handleUnmarshalError(r, w, err)
return
}
resp := s.api.MapReadState(r.Context(), req)
if s.config.UseOpenTelemetry && resp.Error != nil {
span := trace.SpanFromContext(r.Context())
span.SetStatus(codes.Error, resp.Error.Error())
}
if resp.Error != nil && s.useTransportErrorMode(r) {
metrics.IncAPIError(s.api.config.Protocol, "map_read_state", resp.Error.Code)
statusCode := MapErrorToHTTPCode(resp.Error)
data, _ = EncodeError(resp.Error)
s.writeJsonCustomStatus(w, statusCode, data)
return
}
data, err = responseEncoder.EncodeMapReadState(resp)
if err != nil {
metrics.IncAPIErrorStringCode(s.api.config.Protocol, "map_read_state", "marshal")
s.handleMarshalError(r, w, err)
return
}
if resp.Error != nil {
metrics.IncAPIError(s.api.config.Protocol, "map_read_state", resp.Error.Code)
}
s.writeJson(w, data)
}
func (s *Handler) handleMapReadStream(w http.ResponseWriter, r *http.Request) {
data, err := io.ReadAll(r.Body)
if err != nil {
metrics.IncAPIErrorStringCode(s.api.config.Protocol, "map_read_stream", "read_body")
s.handleReadDataError(r, w, err)
return
}
req, err := requestDecoder.DecodeMapReadStream(data)
if err != nil {
metrics.IncAPIErrorStringCode(s.api.config.Protocol, "map_read_stream", "unmarshal")
s.handleUnmarshalError(r, w, err)
return
}
resp := s.api.MapReadStream(r.Context(), req)
if s.config.UseOpenTelemetry && resp.Error != nil {
span := trace.SpanFromContext(r.Context())
span.SetStatus(codes.Error, resp.Error.Error())
}
if resp.Error != nil && s.useTransportErrorMode(r) {
metrics.IncAPIError(s.api.config.Protocol, "map_read_stream", resp.Error.Code)
statusCode := MapErrorToHTTPCode(resp.Error)
data, _ = EncodeError(resp.Error)
s.writeJsonCustomStatus(w, statusCode, data)
return
}
data, err = responseEncoder.EncodeMapReadStream(resp)
if err != nil {
metrics.IncAPIErrorStringCode(s.api.config.Protocol, "map_read_stream", "marshal")
s.handleMarshalError(r, w, err)
return
}
if resp.Error != nil {
metrics.IncAPIError(s.api.config.Protocol, "map_read_stream", resp.Error.Code)
}
s.writeJson(w, data)
}
func (s *Handler) handleMapStats(w http.ResponseWriter, r *http.Request) {
data, err := io.ReadAll(r.Body)
if err != nil {
metrics.IncAPIErrorStringCode(s.api.config.Protocol, "map_stats", "read_body")
s.handleReadDataError(r, w, err)
return
}
req, err := requestDecoder.DecodeMapStats(data)
if err != nil {
metrics.IncAPIErrorStringCode(s.api.config.Protocol, "map_stats", "unmarshal")
s.handleUnmarshalError(r, w, err)
return
}
resp := s.api.MapStats(r.Context(), req)
if s.config.UseOpenTelemetry && resp.Error != nil {
span := trace.SpanFromContext(r.Context())
span.SetStatus(codes.Error, resp.Error.Error())
}
if resp.Error != nil && s.useTransportErrorMode(r) {
metrics.IncAPIError(s.api.config.Protocol, "map_stats", resp.Error.Code)
statusCode := MapErrorToHTTPCode(resp.Error)
data, _ = EncodeError(resp.Error)
s.writeJsonCustomStatus(w, statusCode, data)
return
}
data, err = responseEncoder.EncodeMapStats(resp)
if err != nil {
metrics.IncAPIErrorStringCode(s.api.config.Protocol, "map_stats", "marshal")
s.handleMarshalError(r, w, err)
return
}
if resp.Error != nil {
metrics.IncAPIError(s.api.config.Protocol, "map_stats", resp.Error.Code)
}
s.writeJson(w, data)
}
func (s *Handler) handleMapClear(w http.ResponseWriter, r *http.Request) {
data, err := io.ReadAll(r.Body)
if err != nil {
metrics.IncAPIErrorStringCode(s.api.config.Protocol, "map_clear", "read_body")
s.handleReadDataError(r, w, err)
return
}
req, err := requestDecoder.DecodeMapClear(data)
if err != nil {
metrics.IncAPIErrorStringCode(s.api.config.Protocol, "map_clear", "unmarshal")
s.handleUnmarshalError(r, w, err)
return
}
resp := s.api.MapClear(r.Context(), req)
if s.config.UseOpenTelemetry && resp.Error != nil {
span := trace.SpanFromContext(r.Context())
span.SetStatus(codes.Error, resp.Error.Error())
}
if resp.Error != nil && s.useTransportErrorMode(r) {
metrics.IncAPIError(s.api.config.Protocol, "map_clear", resp.Error.Code)
statusCode := MapErrorToHTTPCode(resp.Error)
data, _ = EncodeError(resp.Error)
s.writeJsonCustomStatus(w, statusCode, data)
return
}
data, err = responseEncoder.EncodeMapClear(resp)
if err != nil {
metrics.IncAPIErrorStringCode(s.api.config.Protocol, "map_clear", "marshal")
s.handleMarshalError(r, w, err)
return
}
if resp.Error != nil {
metrics.IncAPIError(s.api.config.Protocol, "map_clear", resp.Error.Code)
}
s.writeJson(w, data)
}
func (s *Handler) handleSharedPollPublish(w http.ResponseWriter, r *http.Request) {
data, err := io.ReadAll(r.Body)
if err != nil {
metrics.IncAPIErrorStringCode(s.api.config.Protocol, "shared_poll_publish", "read_body")
s.handleReadDataError(r, w, err)
return
}
req, err := requestDecoder.DecodeSharedPollPublish(data)
if err != nil {
metrics.IncAPIErrorStringCode(s.api.config.Protocol, "shared_poll_publish", "unmarshal")
s.handleUnmarshalError(r, w, err)
return
}
resp := s.api.SharedPollPublish(r.Context(), req)
if s.config.UseOpenTelemetry && resp.Error != nil {
span := trace.SpanFromContext(r.Context())
span.SetStatus(codes.Error, resp.Error.Error())
}
if resp.Error != nil && s.useTransportErrorMode(r) {
metrics.IncAPIError(s.api.config.Protocol, "shared_poll_publish", resp.Error.Code)
statusCode := MapErrorToHTTPCode(resp.Error)
data, _ = EncodeError(resp.Error)
s.writeJsonCustomStatus(w, statusCode, data)
return
}
data, err = responseEncoder.EncodeSharedPollPublish(resp)
if err != nil {
metrics.IncAPIErrorStringCode(s.api.config.Protocol, "shared_poll_publish", "marshal")
s.handleMarshalError(r, w, err)
return
}
if resp.Error != nil {
metrics.IncAPIError(s.api.config.Protocol, "shared_poll_publish", resp.Error.Code)
}
s.writeJson(w, data)
}

View File

@@ -0,0 +1,774 @@
package api
import (
"bytes"
"context"
"encoding/json"
"errors"
"io"
"net/http"
"strconv"
"strings"
"sync"
"github.com/centrifugal/centrifugo/v6/internal/apiproto"
"github.com/rs/zerolog/log"
"go.opentelemetry.io/otel/attribute"
"go.opentelemetry.io/otel/codes"
"go.opentelemetry.io/otel/trace"
)
type LegacyCommand struct {
Id uint32 `json:"id,omitempty"`
Method CommandMethodType `json:"method,omitempty"`
Params apiproto.Raw `json:"params,omitempty"`
}
type LegacyReply struct {
Id uint32 `json:"id,omitempty"`
Error *apiproto.Error `json:"error,omitempty"`
Result apiproto.Raw `json:"result,omitempty"`
}
type CommandMethodType int32
// UnmarshalJSON helps to unmarshal command method when set as string.
func (m *CommandMethodType) UnmarshalJSON(data []byte) error {
if len(data) > 2 {
if v, ok := Command_MethodType_value[strings.ToUpper(string(data[1:len(data)-1]))]; ok {
*m = CommandMethodType(v)
return nil
}
}
val, err := strconv.Atoi(string(data))
if err != nil {
return err
}
*m = CommandMethodType(val)
return nil
}
// GetParamsDecoder ...
func GetParamsDecoder() apiproto.RequestDecoder {
return apiproto.NewJSONRequestDecoder()
}
// PutParamsDecoder ...
func PutParamsDecoder(_ apiproto.RequestDecoder) {}
// GetResultEncoder ...
func GetResultEncoder() apiproto.ResultEncoder {
return apiproto.NewJSONResultEncoder()
}
// PutResultEncoder ...
func PutResultEncoder(_ apiproto.ResultEncoder) {}
var (
jsonReplyEncoderPool sync.Pool
)
// ReplyEncoder ...
type ReplyEncoder interface {
Reset()
Encode(reply *LegacyReply) error
Finish() []byte
}
var _ ReplyEncoder = (*JSONReplyEncoder)(nil)
// JSONReplyEncoder ...
type JSONReplyEncoder struct {
count int
buffer bytes.Buffer
}
// NewJSONReplyEncoder ...
func NewJSONReplyEncoder() *JSONReplyEncoder {
return &JSONReplyEncoder{}
}
// Reset ...
func (e *JSONReplyEncoder) Reset() {
e.count = 0
e.buffer.Reset()
}
// Encode ...
func (e *JSONReplyEncoder) Encode(r *LegacyReply) error {
if e.count > 0 {
e.buffer.WriteString("\n")
}
data, err := json.Marshal(r)
if err != nil {
return err
}
e.buffer.Write(data)
e.count++
return nil
}
// Finish ...
func (e *JSONReplyEncoder) Finish() []byte {
data := e.buffer.Bytes()
dataCopy := make([]byte, len(data))
copy(dataCopy, data)
return dataCopy
}
// GetReplyEncoder ...
func GetReplyEncoder() ReplyEncoder {
e := jsonReplyEncoderPool.Get()
if e == nil {
return NewJSONReplyEncoder()
}
return e.(ReplyEncoder)
}
// PutReplyEncoder ...
func PutReplyEncoder(e ReplyEncoder) {
e.Reset()
jsonReplyEncoderPool.Put(e)
}
// CommandDecoder ...
type CommandDecoder interface {
Reset([]byte)
Decode() (*LegacyCommand, error)
}
var jsonCommandDecoderPool sync.Pool
var _ CommandDecoder = (*JSONCommandDecoder)(nil)
// GetCommandDecoder ...
func GetCommandDecoder(data []byte) CommandDecoder {
e := jsonCommandDecoderPool.Get()
if e == nil {
return NewJSONCommandDecoder(data)
}
decoder := e.(CommandDecoder)
decoder.Reset(data)
return decoder
}
// PutCommandDecoder ...
func PutCommandDecoder(d CommandDecoder) {
jsonCommandDecoderPool.Put(d)
}
// JSONCommandDecoder ...
type JSONCommandDecoder struct {
decoder *json.Decoder
}
// NewJSONCommandDecoder ...
func NewJSONCommandDecoder(data []byte) *JSONCommandDecoder {
return &JSONCommandDecoder{
decoder: json.NewDecoder(bytes.NewReader(data)),
}
}
// Reset ...
func (d *JSONCommandDecoder) Reset(data []byte) {
d.decoder = json.NewDecoder(bytes.NewReader(data))
}
// Decode ...
func (d *JSONCommandDecoder) Decode() (*LegacyCommand, error) {
var c LegacyCommand
err := d.decoder.Decode(&c)
if err != nil {
return nil, err
}
return &c, nil
}
// OldRoute handles all methods inside one /api handler.
// The plan is to remove it in Centrifugo v7.
func (s *Handler) OldRoute() http.HandlerFunc {
return s.handleAPI
}
func (s *Handler) handleAPI(w http.ResponseWriter, r *http.Request) {
select {
case <-s.node.NotifyShutdown():
w.WriteHeader(http.StatusServiceUnavailable)
return
default:
}
var data []byte
var err error
data, err = io.ReadAll(r.Body)
if err != nil {
log.Error().Err(err).Msg("error reading API request body")
http.Error(w, "Internal Server Error", http.StatusInternalServerError)
return
}
if len(data) == 0 {
log.Error().Err(errors.New("no data in API request")).Msg("bad request")
http.Error(w, "Bad Request", http.StatusBadRequest)
return
}
encoder := GetReplyEncoder()
defer PutReplyEncoder(encoder)
decoder := GetCommandDecoder(data)
defer PutCommandDecoder(decoder)
for {
command, decodeErr := decoder.Decode()
if decodeErr != nil && decodeErr != io.EOF {
log.Error().Err(decodeErr).Msg("error decoding API data")
http.Error(w, "Bad Request", http.StatusBadRequest)
return
}
if command != nil {
if s.config.UseOpenTelemetry {
span := trace.SpanFromContext(r.Context())
span.SetAttributes(attribute.String("method", Command_MethodType_name[int32(command.Method)]))
}
rep, err := s.handleAPICommand(r.Context(), command)
if err != nil {
if s.config.UseOpenTelemetry {
span := trace.SpanFromContext(r.Context())
span.SetStatus(codes.Error, err.Error())
}
log.Error().Err(err).Msg("error handling API command")
http.Error(w, "Internal Server Error", http.StatusInternalServerError)
return
}
if s.config.UseOpenTelemetry && rep.Error != nil {
span := trace.SpanFromContext(r.Context())
span.SetStatus(codes.Error, rep.Error.Error())
}
err = encoder.Encode(rep)
if err != nil {
log.Error().Err(err).Msg("error encoding API reply")
http.Error(w, "Internal Server Error", http.StatusInternalServerError)
return
}
}
if decodeErr == io.EOF {
break
}
}
s.writeJson(w, encoder.Finish())
}
const (
Command_PUBLISH CommandMethodType = 0
Command_BROADCAST CommandMethodType = 1
Command_UNSUBSCRIBE CommandMethodType = 2
Command_DISCONNECT CommandMethodType = 3
Command_PRESENCE CommandMethodType = 4
Command_PRESENCE_STATS CommandMethodType = 5
Command_HISTORY CommandMethodType = 6
Command_HISTORY_REMOVE CommandMethodType = 7
Command_CHANNELS CommandMethodType = 8
Command_INFO CommandMethodType = 9
Command_RPC CommandMethodType = 10
Command_SUBSCRIBE CommandMethodType = 11
Command_REFRESH CommandMethodType = 12
Command_CONNECTIONS CommandMethodType = 14
Command_UPDATE_USER_STATUS CommandMethodType = 15
Command_GET_USER_STATUS CommandMethodType = 16
Command_DELETE_USER_STATUS CommandMethodType = 17
Command_BLOCK_USER CommandMethodType = 18
Command_UNBLOCK_USER CommandMethodType = 19
Command_REVOKE_TOKEN CommandMethodType = 20
Command_INVALIDATE_USER_TOKENS CommandMethodType = 21
Command_DEVICE_REGISTER CommandMethodType = 22
Command_DEVICE_UPDATE CommandMethodType = 23
Command_DEVICE_REMOVE CommandMethodType = 24
Command_DEVICE_LIST CommandMethodType = 25
Command_DEVICE_TOPIC_LIST CommandMethodType = 26
Command_DEVICE_TOPIC_UPDATE CommandMethodType = 27
Command_USER_TOPIC_LIST CommandMethodType = 28
Command_USER_TOPIC_UPDATE CommandMethodType = 29
Command_SEND_PUSH_NOTIFICATION CommandMethodType = 30
Command_UPDATE_PUSH_STATUS CommandMethodType = 31
Command_CANCEL_PUSH CommandMethodType = 32
Command_MAP_PUBLISH CommandMethodType = 33
Command_MAP_REMOVE CommandMethodType = 34
Command_MAP_READ_STATE CommandMethodType = 35
Command_MAP_READ_STREAM CommandMethodType = 36
Command_MAP_STATS CommandMethodType = 37
Command_MAP_CLEAR CommandMethodType = 38
Command_SHARED_POLL_PUBLISH CommandMethodType = 39
)
var Command_MethodType_value = map[string]int32{
"PUBLISH": 0,
"BROADCAST": 1,
"UNSUBSCRIBE": 2,
"DISCONNECT": 3,
"PRESENCE": 4,
"PRESENCE_STATS": 5,
"HISTORY": 6,
"HISTORY_REMOVE": 7,
"CHANNELS": 8,
"INFO": 9,
"RPC": 10,
"SUBSCRIBE": 11,
"REFRESH": 12,
"CONNECTIONS": 14,
"UPDATE_USER_STATUS": 15,
"GET_USER_STATUS": 16,
"DELETE_USER_STATUS": 17,
"BLOCK_USER": 18,
"UNBLOCK_USER": 19,
"REVOKE_TOKEN": 20,
"INVALIDATE_USER_TOKENS": 21,
"DEVICE_REGISTER": 22,
"DEVICE_UPDATE": 23,
"DEVICE_REMOVE": 24,
"DEVICE_LIST": 25,
"DEVICE_TOPIC_LIST": 26,
"DEVICE_TOPIC_UPDATE": 27,
"USER_TOPIC_LIST": 28,
"USER_TOPIC_UPDATE": 29,
"SEND_PUSH_NOTIFICATION": 30,
"UPDATE_PUSH_STATUS": 31,
"CANCEL_PUSH": 32,
"MAP_PUBLISH": 33,
"MAP_REMOVE": 34,
"MAP_READ_STATE": 35,
"MAP_READ_STREAM": 36,
"MAP_STATS": 37,
"MAP_CLEAR": 38,
"SHARED_POLL_PUBLISH": 39,
}
var (
Command_MethodType_name = map[int32]string{
0: "PUBLISH",
1: "BROADCAST",
2: "UNSUBSCRIBE",
3: "DISCONNECT",
4: "PRESENCE",
5: "PRESENCE_STATS",
6: "HISTORY",
7: "HISTORY_REMOVE",
8: "CHANNELS",
9: "INFO",
10: "RPC",
11: "SUBSCRIBE",
12: "REFRESH",
14: "CONNECTIONS",
15: "UPDATE_USER_STATUS",
16: "GET_USER_STATUS",
17: "DELETE_USER_STATUS",
18: "BLOCK_USER",
19: "UNBLOCK_USER",
20: "REVOKE_TOKEN",
21: "INVALIDATE_USER_TOKENS",
22: "DEVICE_REGISTER",
23: "DEVICE_UPDATE",
24: "DEVICE_REMOVE",
25: "DEVICE_LIST",
26: "DEVICE_TOPIC_LIST",
27: "DEVICE_TOPIC_UPDATE",
28: "USER_TOPIC_LIST",
29: "USER_TOPIC_UPDATE",
30: "SEND_PUSH_NOTIFICATION",
31: "UPDATE_PUSH_STATUS",
32: "CANCEL_PUSH",
33: "MAP_PUBLISH",
34: "MAP_REMOVE",
35: "MAP_READ_STATE",
36: "MAP_READ_STREAM",
37: "MAP_STATS",
38: "MAP_CLEAR",
39: "SHARED_POLL_PUBLISH",
}
)
func (s *Handler) handleAPICommand(ctx context.Context, cmd *LegacyCommand) (*LegacyReply, error) {
method := cmd.Method
params := cmd.Params
rep := &LegacyReply{
Id: cmd.Id,
}
var replyRes apiproto.Raw
decoder := GetParamsDecoder()
defer PutParamsDecoder(decoder)
encoder := GetResultEncoder()
defer PutResultEncoder(encoder)
switch method {
case Command_PUBLISH:
cmd, err := decoder.DecodePublish(params)
if err != nil {
log.Error().Err(err).Msg("error decoding publish params")
rep.Error = apiproto.ErrorBadRequest
return rep, nil
}
resp := s.api.Publish(ctx, cmd)
if resp.Error != nil {
rep.Error = resp.Error
} else {
if resp.Result != nil {
replyRes, err = encoder.EncodePublish(resp.Result)
if err != nil {
return nil, err
}
}
}
case Command_BROADCAST:
cmd, err := decoder.DecodeBroadcast(params)
if err != nil {
log.Error().Err(err).Msg("error decoding broadcast params")
rep.Error = apiproto.ErrorBadRequest
return rep, nil
}
resp := s.api.Broadcast(ctx, cmd)
if resp.Error != nil {
rep.Error = resp.Error
} else {
if resp.Result != nil {
replyRes, err = encoder.EncodeBroadcast(resp.Result)
if err != nil {
return nil, err
}
}
}
case Command_SUBSCRIBE:
cmd, err := decoder.DecodeSubscribe(params)
if err != nil {
log.Error().Err(err).Msg("error decoding subscribe params")
rep.Error = apiproto.ErrorBadRequest
return rep, nil
}
resp := s.api.Subscribe(ctx, cmd)
if resp.Error != nil {
rep.Error = resp.Error
} else {
if resp.Result != nil {
replyRes, err = encoder.EncodeSubscribe(resp.Result)
if err != nil {
return nil, err
}
}
}
case Command_UNSUBSCRIBE:
cmd, err := decoder.DecodeUnsubscribe(params)
if err != nil {
log.Error().Err(err).Msg("error decoding unsubscribe params")
rep.Error = apiproto.ErrorBadRequest
return rep, nil
}
resp := s.api.Unsubscribe(ctx, cmd)
if resp.Error != nil {
rep.Error = resp.Error
} else {
if resp.Result != nil {
replyRes, err = encoder.EncodeUnsubscribe(resp.Result)
if err != nil {
return nil, err
}
}
}
case Command_DISCONNECT:
cmd, err := decoder.DecodeDisconnect(params)
if err != nil {
log.Error().Err(err).Msg("error decoding disconnect params")
rep.Error = apiproto.ErrorBadRequest
return rep, nil
}
resp := s.api.Disconnect(ctx, cmd)
if resp.Error != nil {
rep.Error = resp.Error
} else {
if resp.Result != nil {
replyRes, err = encoder.EncodeDisconnect(resp.Result)
if err != nil {
return nil, err
}
}
}
case Command_PRESENCE:
cmd, err := decoder.DecodePresence(params)
if err != nil {
log.Error().Err(err).Msg("error decoding presence params")
rep.Error = apiproto.ErrorBadRequest
return rep, nil
}
resp := s.api.Presence(ctx, cmd)
if resp.Error != nil {
rep.Error = resp.Error
} else {
if resp.Result != nil {
replyRes, err = encoder.EncodePresence(resp.Result)
if err != nil {
return nil, err
}
}
}
case Command_PRESENCE_STATS:
cmd, err := decoder.DecodePresenceStats(params)
if err != nil {
log.Error().Err(err).Msg("error decoding presence stats params")
rep.Error = apiproto.ErrorBadRequest
return rep, nil
}
resp := s.api.PresenceStats(ctx, cmd)
if resp.Error != nil {
rep.Error = resp.Error
} else {
if resp.Result != nil {
replyRes, err = encoder.EncodePresenceStats(resp.Result)
if err != nil {
return nil, err
}
}
}
case Command_HISTORY:
cmd, err := decoder.DecodeHistory(params)
if err != nil {
log.Error().Err(err).Msg("error decoding history params")
rep.Error = apiproto.ErrorBadRequest
return rep, nil
}
resp := s.api.History(ctx, cmd)
if resp.Error != nil {
rep.Error = resp.Error
} else {
if resp.Result != nil {
replyRes, err = encoder.EncodeHistory(resp.Result)
if err != nil {
return nil, err
}
}
}
case Command_HISTORY_REMOVE:
cmd, err := decoder.DecodeHistoryRemove(params)
if err != nil {
log.Error().Err(err).Msg("error decoding history remove params")
rep.Error = apiproto.ErrorBadRequest
return rep, nil
}
resp := s.api.HistoryRemove(ctx, cmd)
if resp.Error != nil {
rep.Error = resp.Error
} else {
if resp.Result != nil {
replyRes, err = encoder.EncodeHistoryRemove(resp.Result)
if err != nil {
return nil, err
}
}
}
case Command_INFO:
resp := s.api.Info(ctx, &apiproto.InfoRequest{})
if resp.Error != nil {
rep.Error = resp.Error
} else {
if resp.Result != nil {
var err error
replyRes, err = encoder.EncodeInfo(resp.Result)
if err != nil {
return nil, err
}
}
}
case Command_RPC:
cmd, err := decoder.DecodeRPC(params)
if err != nil {
log.Error().Err(err).Msg("error decoding rpc params")
rep.Error = apiproto.ErrorBadRequest
return rep, nil
}
resp := s.api.RPC(ctx, cmd)
if resp.Error != nil {
rep.Error = resp.Error
} else {
if resp.Result != nil {
replyRes, err = encoder.EncodeRPC(resp.Result)
if err != nil {
return nil, err
}
}
}
case Command_REFRESH:
cmd, err := decoder.DecodeRefresh(params)
if err != nil {
log.Error().Err(err).Msg("error decoding refresh params")
rep.Error = apiproto.ErrorBadRequest
return rep, nil
}
resp := s.api.Refresh(ctx, cmd)
if resp.Error != nil {
rep.Error = resp.Error
} else {
if resp.Result != nil {
replyRes, err = encoder.EncodeRefresh(resp.Result)
if err != nil {
return nil, err
}
}
}
case Command_CHANNELS:
cmd, err := decoder.DecodeChannels(params)
if err != nil {
log.Error().Err(err).Msg("error decoding channels params")
rep.Error = apiproto.ErrorBadRequest
return rep, nil
}
resp := s.api.Channels(ctx, cmd)
if resp.Error != nil {
rep.Error = resp.Error
} else {
if resp.Result != nil {
replyRes, err = encoder.EncodeChannels(resp.Result)
if err != nil {
return nil, err
}
}
}
case Command_MAP_PUBLISH:
cmd, err := decoder.DecodeMapPublish(params)
if err != nil {
log.Error().Err(err).Msg("error decoding map publish params")
rep.Error = apiproto.ErrorBadRequest
return rep, nil
}
resp := s.api.MapPublish(ctx, cmd)
if resp.Error != nil {
rep.Error = resp.Error
} else {
if resp.Result != nil {
replyRes, err = encoder.EncodeMapPublish(resp.Result)
if err != nil {
return nil, err
}
}
}
case Command_MAP_REMOVE:
cmd, err := decoder.DecodeMapRemove(params)
if err != nil {
log.Error().Err(err).Msg("error decoding map remove params")
rep.Error = apiproto.ErrorBadRequest
return rep, nil
}
resp := s.api.MapRemove(ctx, cmd)
if resp.Error != nil {
rep.Error = resp.Error
} else {
if resp.Result != nil {
replyRes, err = encoder.EncodeMapRemove(resp.Result)
if err != nil {
return nil, err
}
}
}
case Command_MAP_READ_STATE:
cmd, err := decoder.DecodeMapReadState(params)
if err != nil {
log.Error().Err(err).Msg("error decoding map read state params")
rep.Error = apiproto.ErrorBadRequest
return rep, nil
}
resp := s.api.MapReadState(ctx, cmd)
if resp.Error != nil {
rep.Error = resp.Error
} else {
if resp.Result != nil {
replyRes, err = encoder.EncodeMapReadState(resp.Result)
if err != nil {
return nil, err
}
}
}
case Command_MAP_READ_STREAM:
cmd, err := decoder.DecodeMapReadStream(params)
if err != nil {
log.Error().Err(err).Msg("error decoding map read stream params")
rep.Error = apiproto.ErrorBadRequest
return rep, nil
}
resp := s.api.MapReadStream(ctx, cmd)
if resp.Error != nil {
rep.Error = resp.Error
} else {
if resp.Result != nil {
replyRes, err = encoder.EncodeMapReadStream(resp.Result)
if err != nil {
return nil, err
}
}
}
case Command_MAP_STATS:
cmd, err := decoder.DecodeMapStats(params)
if err != nil {
log.Error().Err(err).Msg("error decoding map stats params")
rep.Error = apiproto.ErrorBadRequest
return rep, nil
}
resp := s.api.MapStats(ctx, cmd)
if resp.Error != nil {
rep.Error = resp.Error
} else {
if resp.Result != nil {
replyRes, err = encoder.EncodeMapStats(resp.Result)
if err != nil {
return nil, err
}
}
}
case Command_MAP_CLEAR:
cmd, err := decoder.DecodeMapClear(params)
if err != nil {
log.Error().Err(err).Msg("error decoding map clear params")
rep.Error = apiproto.ErrorBadRequest
return rep, nil
}
resp := s.api.MapClear(ctx, cmd)
if resp.Error != nil {
rep.Error = resp.Error
} else {
if resp.Result != nil {
replyRes, err = encoder.EncodeMapClear(resp.Result)
if err != nil {
return nil, err
}
}
}
case Command_SHARED_POLL_PUBLISH:
cmd, err := decoder.DecodeSharedPollPublish(params)
if err != nil {
log.Error().Err(err).Msg("error decoding shared poll publish params")
rep.Error = apiproto.ErrorBadRequest
return rep, nil
}
resp := s.api.SharedPollPublish(ctx, cmd)
if resp.Error != nil {
rep.Error = resp.Error
} else {
if resp.Result != nil {
replyRes, err = encoder.EncodeSharedPollPublish(resp.Result)
if err != nil {
return nil, err
}
}
}
default:
rep.Error = apiproto.ErrorNotFound
}
if replyRes != nil {
rep.Result = replyRes
}
return rep, nil
}

View File

@@ -0,0 +1,106 @@
package api
import (
"bytes"
"context"
"net/http"
"net/http/httptest"
"strings"
"testing"
"github.com/centrifugal/centrifugo/v6/internal/config"
"github.com/stretchr/testify/require"
)
func TestAPIHandler(t *testing.T) {
n := nodeWithMemoryEngine()
defer func() { _ = n.Shutdown(context.Background()) }()
cfg := config.DefaultConfig()
cfgContainer, err := config.NewContainer(cfg)
require.NoError(t, err)
apiExecutor := NewExecutor(n, cfgContainer, &testSurveyCaller{}, ExecutorConfig{Protocol: "test", UseOpenTelemetry: false})
mux := http.NewServeMux()
apiHandler := NewHandler(n, apiExecutor, Config{})
mux.Handle("/api", apiHandler.OldRoute())
for path, handler := range apiHandler.Routes() {
handlePath := "/api" + path
mux.Handle(handlePath, handler)
}
server := httptest.NewServer(mux)
defer server.Close()
// nil body.
req, _ := http.NewRequest("POST", server.URL+"/api", nil)
resp, err := http.DefaultClient.Do(req)
require.NoError(t, err)
require.Equal(t, resp.StatusCode, http.StatusBadRequest)
_ = resp.Body.Close()
// empty body.
req, _ = http.NewRequest("POST", server.URL+"/api", strings.NewReader(""))
resp, err = http.DefaultClient.Do(req)
require.NoError(t, err)
require.Equal(t, resp.StatusCode, http.StatusBadRequest)
_ = resp.Body.Close()
// valid JSON request.
data := `{"method":"publish","params":{"channel": "test", "data":{}}}`
req, _ = http.NewRequest("POST", server.URL+"/api", bytes.NewBuffer([]byte(data)))
req.Header.Add("Content-Type", "application/json")
resp, err = http.DefaultClient.Do(req)
require.NoError(t, err)
require.Equal(t, resp.StatusCode, http.StatusOK)
_ = resp.Body.Close()
// valid JSON request to special method route
data = `{"channel": "test", "data":{}}`
req, _ = http.NewRequest("POST", server.URL+"/api/publish", bytes.NewBuffer([]byte(data)))
req.Header.Add("Content-Type", "application/json")
resp, err = http.DefaultClient.Do(req)
require.NoError(t, err)
require.Equal(t, resp.StatusCode, http.StatusOK)
_ = resp.Body.Close()
// request with unknown method.
data = `{"method":"unknown","params":{"channel": "test", "data":{}}}`
req, _ = http.NewRequest("POST", server.URL+"/api", bytes.NewBuffer([]byte(data)))
req.Header.Add("Content-Type", "application/json")
resp, err = http.DefaultClient.Do(req)
require.NoError(t, err)
require.Equal(t, resp.StatusCode, http.StatusBadRequest)
_ = resp.Body.Close()
}
func BenchmarkAPIHandler(b *testing.B) {
n := nodeWithMemoryEngine()
defer func() { _ = n.Shutdown(context.Background()) }()
cfg := config.DefaultConfig()
cfg.Client.Insecure = true
cfgContainer, err := config.NewContainer(cfg)
require.NoError(b, err)
handler := NewHandler(n, NewExecutor(n, cfgContainer, nil, ExecutorConfig{
Protocol: "http",
UseOpenTelemetry: false,
}), Config{})
payload := []byte(`{"method": "publish", "params": {"channel": "index", "data": 1}}`)
b.ReportAllocs()
b.ResetTimer()
b.RunParallel(func(pb *testing.PB) {
for pb.Next() {
request, _ := http.NewRequest(http.MethodPost, "/api", bytes.NewReader(payload))
recorder := httptest.NewRecorder()
handler.handlePublish(recorder, request)
if recorder.Code != http.StatusOK {
b.Fatalf("unexpected status code %d", recorder.Code)
}
}
})
}