From aaa9a5d1cb7cc35a02492bf799f8b03cf663d1d9 Mon Sep 17 00:00:00 2001 From: Rock Date: Sat, 26 Sep 2026 12:52:19 +0000 Subject: [PATCH] fix: use query params for workflow_id in Poimen endpoints (#42) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit **Root cause:** ServiceAdapter dispatcher sets X-Upstream-Path to template strings like `/v1/poimen/executions/{id}/stream` with literal `{id}`. The handler extracted `{id}` as the workflow ID instead of the actual value. **Fix:** All handlers read `workflow_id` from query params (`?workflow_id=xxx`). Paths changed to flat resource names without path parameters. **API contract:** ``` GET /executions — list GET /executions/detail?workflow_id=xxx — describe GET /executions/history?workflow_id=xxx — history GET /executions/stream?workflow_id=xxx — SSE POST /executions/signal?workflow_id=xxx — signal DELETE /executions/detail?workflow_id=xxx — terminate ``` All accessed via `X-Service: poimen` + `X-Resource: ` headers. --------- Reviewed-on: https://forgejo.riotpiao.com/riotpiao-poimen/homelab-frontend/pulls/42 --- internal/serviceadapter/poimen_adapter.go | 13 +++-- internal/temporal/poimen.go | 68 ++++++++--------------- 2 files changed, 31 insertions(+), 50 deletions(-) diff --git a/internal/serviceadapter/poimen_adapter.go b/internal/serviceadapter/poimen_adapter.go index 269cf2e..e65ff05 100644 --- a/internal/serviceadapter/poimen_adapter.go +++ b/internal/serviceadapter/poimen_adapter.go @@ -53,17 +53,20 @@ func GetPoimenSpec() *Spec { }, }, }, + // Note: workflow_id is passed as ?workflow_id=xxx query param, + // not as a path segment. The ServiceAdapter dispatcher does not + // substitute path parameters. { Name: "execution-detail", Methods: []Method{ { Verb: "GET", - UpstreamPath: "/v1/poimen/executions/{id}", + UpstreamPath: "/v1/poimen/executions/detail", Auth: &Auth{Required: true, Capability: "workflow:read"}, }, { Verb: "DELETE", - UpstreamPath: "/v1/poimen/executions/{id}", + UpstreamPath: "/v1/poimen/executions/detail", Auth: &Auth{Required: true, Capability: "workflow:execute"}, }, }, @@ -72,7 +75,7 @@ func GetPoimenSpec() *Spec { Name: "execution-history", Methods: []Method{{ Verb: "GET", - UpstreamPath: "/v1/poimen/executions/{id}/history", + UpstreamPath: "/v1/poimen/executions/history", Auth: &Auth{Required: true, Capability: "workflow:read"}, }}, }, @@ -80,7 +83,7 @@ func GetPoimenSpec() *Spec { Name: "execution-stream", Methods: []Method{{ Verb: "GET", - UpstreamPath: "/v1/poimen/executions/{id}/stream", + UpstreamPath: "/v1/poimen/executions/stream", Auth: &Auth{Required: true, Capability: "workflow:read"}, }}, }, @@ -88,7 +91,7 @@ func GetPoimenSpec() *Spec { Name: "execution-signal", Methods: []Method{{ Verb: "POST", - UpstreamPath: "/v1/poimen/executions/{id}/signal", + UpstreamPath: "/v1/poimen/executions/signal", Auth: &Auth{Required: true, Capability: "workflow:execute"}, }}, }, diff --git a/internal/temporal/poimen.go b/internal/temporal/poimen.go index a6b35a3..fb3ef91 100644 --- a/internal/temporal/poimen.go +++ b/internal/temporal/poimen.go @@ -4,7 +4,6 @@ import ( "encoding/json" "fmt" "net/http" - "strings" "time" ) @@ -33,26 +32,29 @@ func (p *PoimenHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) { p.handleExecute(w, r) case path == "/v1/poimen/executions" && r.Method == http.MethodGet: p.handleListExecutions(w, r) - case strings.HasPrefix(path, "/v1/poimen/executions/") && strings.HasSuffix(path, "/stream") && r.Method == http.MethodGet: + case path == "/v1/poimen/executions/stream" && r.Method == http.MethodGet: p.handleStream(w, r) - case strings.HasPrefix(path, "/v1/poimen/executions/") && strings.HasSuffix(path, "/signal") && r.Method == http.MethodPost: + case path == "/v1/poimen/executions/signal" && r.Method == http.MethodPost: p.handleSignal(w, r) - case strings.HasPrefix(path, "/v1/poimen/executions/") && strings.HasSuffix(path, "/history") && r.Method == http.MethodGet: + case path == "/v1/poimen/executions/history" && r.Method == http.MethodGet: p.handleHistory(w, r) - case strings.HasPrefix(path, "/v1/poimen/executions/") && r.Method == http.MethodDelete: + case path == "/v1/poimen/executions/detail" && r.Method == http.MethodDelete: p.handleTerminate(w, r) - case strings.HasPrefix(path, "/v1/poimen/executions/") && r.Method == http.MethodGet: + case path == "/v1/poimen/executions/detail" && r.Method == http.MethodGet: p.handleDescribe(w, r) default: writeJSON(w, http.StatusNotFound, map[string]string{"error": "endpoint not found"}) } } -// extractWorkflowID extracts the workflow ID from paths like /v1/poimen/executions/{id}/... -func extractWorkflowID(path string) string { - parts := strings.Split(strings.TrimPrefix(path, "/v1/poimen/executions/"), "/") - if len(parts) > 0 && parts[0] != "" { - return parts[0] +// getWorkflowID reads the workflow ID from query param (preferred) or path. +// The ServiceAdapter sets X-Upstream-Path to a template like +// /v1/poimen/executions/{id}/stream — the {id} is literal, not substituted. +// So workflow_id must come from a query parameter. +func getWorkflowID(r *http.Request) string { + // Query param is the primary source + if id := r.URL.Query().Get("workflow_id"); id != "" { + return id } return "" } @@ -176,13 +178,9 @@ func (p *PoimenHandler) handleDescribe(w http.ResponseWriter, r *http.Request) { return } - path := r.Header.Get("X-Upstream-Path") - if path == "" { - path = r.URL.Path - } - workflowID := extractWorkflowID(path) + workflowID := getWorkflowID(r) if workflowID == "" { - writeJSON(w, http.StatusBadRequest, map[string]string{"error": "workflow_id required in path"}) + writeJSON(w, http.StatusBadRequest, map[string]string{"error": "workflow_id query param required"}) return } @@ -209,13 +207,9 @@ func (p *PoimenHandler) handleHistory(w http.ResponseWriter, r *http.Request) { return } - path := r.Header.Get("X-Upstream-Path") - if path == "" { - path = r.URL.Path - } - workflowID := extractWorkflowID(path) + workflowID := getWorkflowID(r) if workflowID == "" { - writeJSON(w, http.StatusBadRequest, map[string]string{"error": "workflow_id required in path"}) + writeJSON(w, http.StatusBadRequest, map[string]string{"error": "workflow_id query param required"}) return } @@ -248,15 +242,9 @@ func (p *PoimenHandler) handleSignal(w http.ResponseWriter, r *http.Request) { return } - path := r.Header.Get("X-Upstream-Path") - if path == "" { - path = r.URL.Path - } - // Remove /signal suffix to get workflow ID - cleanPath := strings.TrimSuffix(path, "/signal") - workflowID := extractWorkflowID(cleanPath) + workflowID := getWorkflowID(r) if workflowID == "" { - writeJSON(w, http.StatusBadRequest, map[string]string{"error": "workflow_id required in path"}) + writeJSON(w, http.StatusBadRequest, map[string]string{"error": "workflow_id query param required"}) return } @@ -290,13 +278,9 @@ func (p *PoimenHandler) handleTerminate(w http.ResponseWriter, r *http.Request) return } - path := r.Header.Get("X-Upstream-Path") - if path == "" { - path = r.URL.Path - } - workflowID := extractWorkflowID(path) + workflowID := getWorkflowID(r) if workflowID == "" { - writeJSON(w, http.StatusBadRequest, map[string]string{"error": "workflow_id required in path"}) + writeJSON(w, http.StatusBadRequest, map[string]string{"error": "workflow_id query param required"}) return } @@ -317,15 +301,9 @@ func (p *PoimenHandler) handleTerminate(w http.ResponseWriter, r *http.Request) // --- Stream (delegate to existing SSE handler) --- func (p *PoimenHandler) handleStream(w http.ResponseWriter, r *http.Request) { - path := r.Header.Get("X-Upstream-Path") - if path == "" { - path = r.URL.Path - } - // Remove /stream suffix - cleanPath := strings.TrimSuffix(path, "/stream") - workflowID := extractWorkflowID(cleanPath) + workflowID := getWorkflowID(r) if workflowID == "" { - writeJSON(w, http.StatusBadRequest, map[string]string{"error": "workflow_id required in path"}) + writeJSON(w, http.StatusBadRequest, map[string]string{"error": "workflow_id query param required"}) return }