From f5ff0c08030c290e814e7819db64ce89c3bf0900 Mon Sep 17 00:00:00 2001 From: Cristian Magherusan-Stanciu Date: Mon, 4 May 2026 19:00:55 +0200 Subject: [PATCH] fix(api): correct async self-invoke payload shape (closes #257 followup) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The async refresh path landed in PR #260 but the Lambda self-invoke payload didn't match what the receiving Lambda expects. The shipped payload was: {"event": "scheduled_recommendations"} But internal/server/handler.go::ScheduledEvent has no "event" field — it has Action, Source, DetailType, Detail. Unmarshalling that JSON gives Action="", and ParseScheduledEvent rejects every event with: unknown scheduled task action: "" Verified live in /aws/lambda/cudly-dev-426fc8af-api request 3befb380-6d46-47e1-954b-69a2b59ea90a: 16:49:40 Received unknown event (size: 37 bytes) 16:49:40 Unknown event type, treating as scheduled event 16:49:40 {"errorMessage":"failed to parse scheduled event: unknown scheduled task action: \"\""} The 37-byte event = the {"event":"scheduled_recommendations"} string exactly. The async self-invoke is firing successfully but every invocation is rejected, so the "Refreshing..." banner never clears and collection never runs — same user-visible symptom (refresh times out + no fresh data) as if the env var or IAM grant was missing. Fix: send the payload shape the dispatcher actually accepts: {"source": "aws.events", "action": "collect_recommendations"} action="collect_recommendations" maps to TaskCollectRecommendations in the ParseScheduledEvent switch. source="aws.events" makes detectLambdaEventType in internal/server/lambda.go classify the invoke consistently with EventBridge cron deliveries that already exercise this code path. --- .../api/handler_recommendations_refresh.go | 25 ++++++++++++++++--- 1 file changed, 21 insertions(+), 4 deletions(-) diff --git a/internal/api/handler_recommendations_refresh.go b/internal/api/handler_recommendations_refresh.go index 5cc0a0d5..ed5fd58e 100644 --- a/internal/api/handler_recommendations_refresh.go +++ b/internal/api/handler_recommendations_refresh.go @@ -139,10 +139,27 @@ func (h *Handler) asyncInvokeSelf(ctx context.Context, functionARN string) error return fmt.Errorf("failed to build Lambda client: %w", err) } - // This payload matches the detectLambdaEventType "scheduled" branch in - // internal/server/lambda.go: `scheduledEvent.Action != ""` → "scheduled". - // HandleScheduledTask then dispatches to TaskCollectRecommendations. - payload, _ := json.Marshal(map[string]string{"event": "scheduled_recommendations"}) + // Payload shape matches internal/server/handler.go::ScheduledEvent + its + // ParseScheduledEvent dispatch table: the Action field selects the + // scheduled-task type. "collect_recommendations" maps to + // TaskCollectRecommendations, which is what we want to fire. + // + // The previous payload `{"event":"scheduled_recommendations"}` had no + // matching field in ScheduledEvent at all — Action unmarshalled as "" + // and ParseScheduledEvent rejected the event with "unknown scheduled + // task action: \"\"". Verified in the deployed Lambda log + // `/aws/lambda/cudly-dev-426fc8af-api` request 3befb380-6d46-... — the + // async self-invoke was firing successfully but the receiving Lambda + // was rejecting every invocation, so collection never actually ran. + // + // Source = "aws.events" so detectLambdaEventType in + // internal/server/lambda.go (which checks Source == "aws.events" || + // Action != "") classifies this consistently with EventBridge cron + // deliveries that already exercise this code path. + payload, _ := json.Marshal(map[string]string{ + "source": "aws.events", + "action": "collect_recommendations", + }) _, err = invoker.Invoke(ctx, &lambda.InvokeInput{ FunctionName: aws.String(functionARN),