diff --git a/internal/relui/templates/task_list.html b/internal/relui/templates/task_list.html
index a291383..73cb594 100644
--- a/internal/relui/templates/task_list.html
+++ b/internal/relui/templates/task_list.html
@@ -76,7 +76,7 @@
{{if .Error.Valid}}
<div class="TaskList-retryTask">
<form
- action="{{baseLink (printf "/workflows/%s/tasks/%s/retry" $workflow.ID .Name)}}"
+ action="{{baseLink (printf "/workflows/%s/tasks/%s/retry" $workflow.ID (.Name|urlPathEscape))}}"
method="post">
<input type="hidden" id="workflow.id" name="workflow.id" value="{{$workflow.ID}}" />
<input
@@ -90,7 +90,7 @@
{{else if and (not .ApprovedAt.Valid) (.ReadyForApproval)}}
<div class="TaskList-approveTask">
<form
- action="{{baseLink (printf "/workflows/%s/tasks/%s/approve" $workflow.ID .Name)}}"
+ action="{{baseLink (printf "/workflows/%s/tasks/%s/approve" $workflow.ID (.Name|urlPathEscape))}}"
method="post">
<input type="hidden" id="workflow.id" name="workflow.id" value="{{$workflow.ID}}" />
<input
diff --git a/internal/relui/web.go b/internal/relui/web.go
index 8d999d1..449d0c8 100644
--- a/internal/relui/web.go
+++ b/internal/relui/web.go
@@ -88,6 +88,7 @@
"prettySize": prettySize,
"sidebarWorkflows": s.sidebarWorkflows,
"unmarshalResultDetail": unmarshalResultDetail,
+ "urlPathEscape": url.PathEscape,
}
s.templates = template.Must(template.New("").Funcs(helpers).ParseFS(templates, "templates/*.html"))
s.homeTmpl = s.mustLookup("home.html")
diff --git a/internal/relui/web_test.go b/internal/relui/web_test.go
index e5bc40d..2828052 100644
--- a/internal/relui/web_test.go
+++ b/internal/relui/web_test.go
@@ -1026,3 +1026,36 @@
}
t.Run("unacld workflow", func(t *testing.T) { testWorkflowACL(t, true, false, false) })
}
+
+// Test that it's possible to retry a task whose name includes characters
+// such as "/" that become different after URL path segment escaping.
+func TestRetryHandlerEscaping(t *testing.T) {
+ s := &Server{m: &metricsRouter{mux: http.NewServeMux()}}
+ var gotTaskName string
+ s.m.HandleFunc("POST /workflows/{id}/tasks/{name}/retry", func(w http.ResponseWriter, req *http.Request) {
+ gotTaskName = req.PathValue("name")
+ })
+ cl := &http.Client{Transport: localRoundTripper{s.m}}
+ resp, err := cl.Post("/workflows/123/tasks/update%20x%2Fbuild/retry", "", nil)
+ if err != nil {
+ t.Fatal(err)
+ }
+ if got, want := resp.StatusCode, http.StatusOK; got != want {
+ t.Fatalf("status code mismatch: got %d, want %d", got, want)
+ }
+ if got, want := gotTaskName, "update x/build"; got != want {
+ t.Errorf("task name mismatch: got %q, want %q", got, want)
+ }
+}
+
+// localRoundTripper is an http.RoundTripper that executes HTTP transactions
+// by using handler directly, instead of going over an HTTP connection.
+type localRoundTripper struct {
+ handler http.Handler
+}
+
+func (l localRoundTripper) RoundTrip(req *http.Request) (*http.Response, error) {
+ w := httptest.NewRecorder()
+ l.handler.ServeHTTP(w, req)
+ return w.Result(), nil
+}