From d2d591143a9118f9a6f7178a6f2658c232f558c7 Mon Sep 17 00:00:00 2001 From: SAY-5 Date: Thu, 21 May 2026 12:51:45 -0700 Subject: [PATCH] fix(websub): handle empty channels map in mapWebSubAPIModelToAPI Fixes #1995 mapWebSubChannelsModelToAPI returned a nil *map for empty/nil input and the caller in mapWebSubAPIModelToAPI then dereferenced that nil pointer when assigning to api.WebSubAPI.Channels (a value-type map). Any Get, List, or Update return path panicked for a WebSub API stored with an empty channels map. Return the value-type map directly so the result is always usable, and drop the now-unneeded pointer indirection at the call site. --- .../src/internal/service/websub_api.go | 12 ++-- .../src/internal/service/websub_api_test.go | 58 +++++++++++++++++++ 2 files changed, 64 insertions(+), 6 deletions(-) diff --git a/platform-api/src/internal/service/websub_api.go b/platform-api/src/internal/service/websub_api.go index 893c95b3e..aa20977fa 100644 --- a/platform-api/src/internal/service/websub_api.go +++ b/platform-api/src/internal/service/websub_api.go @@ -414,7 +414,7 @@ func mapWebSubAPIModelToAPI(m *model.WebSubAPI, apiUtil *utils.APIUtil) *api.Web Transport: transport, Context: m.Configuration.Context, Upstream: mapUpstreamModelToAPI(&m.Configuration.Upstream), - Channels: *mapWebSubChannelsModelToAPI(m.Configuration.Channels), + Channels: mapWebSubChannelsModelToAPI(m.Configuration.Channels), AllChannels: mapWebSubAllChannelPoliciesModelToAPI(m.Configuration.AllChannels), SubscriptionPlans: subscriptionPlans, CreatedAt: utils.TimePtr(m.CreatedAt), @@ -465,10 +465,10 @@ func mapWebSubAllChannelPoliciesAPIToModel(in *api.WebSubAllChannelPolicies) *mo } // mapWebSubChannelsModelToAPI converts the model channel map to the API channel map. -func mapWebSubChannelsModelToAPI(in map[string]model.WebSubChannel) *map[string]api.WebSubChannel { - if len(in) == 0 { - return nil - } +// It always returns a non-nil map so callers that embed the result by value +// (api.WebSubAPI.Channels is a value-type map) do not panic when the input is +// empty or nil. +func mapWebSubChannelsModelToAPI(in map[string]model.WebSubChannel) map[string]api.WebSubChannel { out := make(map[string]api.WebSubChannel, len(in)) for name, ch := range in { out[name] = api.WebSubChannel{ @@ -478,7 +478,7 @@ func mapWebSubChannelsModelToAPI(in map[string]model.WebSubChannel) *map[string] OnMessageDelivery: mapEventPoliciesModelToAPI(ch.OnMessageDelivery), } } - return &out + return out } // mapEventPoliciesModelToAPI converts model event policies to API. diff --git a/platform-api/src/internal/service/websub_api_test.go b/platform-api/src/internal/service/websub_api_test.go index 4a8b2ad88..d69212a12 100644 --- a/platform-api/src/internal/service/websub_api_test.go +++ b/platform-api/src/internal/service/websub_api_test.go @@ -491,3 +491,61 @@ func TestWebSubAPI_NilPoliciesHandled(t *testing.T) { t.Errorf("expected nil for nil input, got %v", got) } } + +// TestWebSubAPI_MapModelToAPI_EmptyChannelsDoesNotPanic guards the nil-pointer +// dereference reported in #1995. Previously mapWebSubChannelsModelToAPI +// returned nil for empty/nil channel maps and the caller dereferenced that nil +// pointer when assigning to api.WebSubAPI.Channels (a value-type map). The Get, +// List, and Update return paths panicked for any WebSub API stored with an +// empty channels map. +func TestWebSubAPI_MapModelToAPI_EmptyChannelsDoesNotPanic(t *testing.T) { + tests := []struct { + name string + in map[string]model.WebSubChannel + }{ + {name: "nil channel map", in: nil}, + {name: "empty channel map", in: map[string]model.WebSubChannel{}}, + } + + for _, tc := range tests { + t.Run(tc.name, func(t *testing.T) { + defer func() { + if r := recover(); r != nil { + t.Fatalf("mapWebSubAPIModelToAPI panicked: %v", r) + } + }() + + m := &model.WebSubAPI{ + Handle: "test", + Name: "test", + Version: "v1", + Configuration: model.WebSubAPIConfiguration{ + Channels: tc.in, + }, + } + + got := mapWebSubAPIModelToAPI(m, &utils.APIUtil{}) + if got == nil { + t.Fatal("expected non-nil WebSubAPI result") + } + if got.Channels == nil { + t.Errorf("expected non-nil Channels map, got nil") + } + if len(got.Channels) != 0 { + t.Errorf("expected empty Channels map, got %d entries", len(got.Channels)) + } + }) + } +} + +// TestWebSubAPI_MapChannelsModelToAPI_NeverReturnsNil ensures the helper itself +// always returns a usable (non-nil) map so callers do not need a nil guard +// before assigning the result to a value-type map field. +func TestWebSubAPI_MapChannelsModelToAPI_NeverReturnsNil(t *testing.T) { + if got := mapWebSubChannelsModelToAPI(nil); got == nil { + t.Errorf("expected non-nil map for nil input, got nil") + } + if got := mapWebSubChannelsModelToAPI(map[string]model.WebSubChannel{}); got == nil { + t.Errorf("expected non-nil map for empty input, got nil") + } +}