fix: use query params for workflow_id in Poimen endpoints (#42)
CI / CI (push) Successful in 3m7s
CI / CI (push) Successful in 3m7s
**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: <name>` headers.
---------
Reviewed-on: #42
This commit was merged in pull request #42.
This commit is contained in:
@@ -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"},
|
||||
}},
|
||||
},
|
||||
|
||||
+23
-45
@@ -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
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user