diff --git a/go.mod b/go.mod
index 9de1606..db5673e 100644
--- a/go.mod
+++ b/go.mod
@@ -42,7 +42,6 @@
github.com/jackc/pgconn v1.14.3
github.com/jackc/pgx/v4 v4.18.3
github.com/jellevandenhooff/dkim v0.0.0-20150330215556-f50fe3d243e1
- github.com/julienschmidt/httprouter v1.3.0
github.com/kballard/go-shellquote v0.0.0-20180428030007-95032a82bc51
github.com/mailjet/mailjet-apiv3-go/v4 v4.0.7
github.com/mattn/go-sqlite3 v1.14.14
@@ -122,6 +121,7 @@
github.com/jackc/pgtype v1.14.0 // indirect
github.com/jackc/puddle v1.3.0 // indirect
github.com/jmespath/go-jmespath v0.4.0 // indirect
+ github.com/julienschmidt/httprouter v1.3.0 // indirect
github.com/klauspost/asmfmt v1.3.2 // indirect
github.com/klauspost/compress v1.16.7 // indirect
github.com/klauspost/cpuid/v2 v2.0.9 // indirect
diff --git a/internal/relui/metrics.go b/internal/relui/metrics.go
index 79c6460..b7c9f06 100644
--- a/internal/relui/metrics.go
+++ b/internal/relui/metrics.go
@@ -6,16 +6,13 @@
import (
"context"
- "fmt"
- "mime"
+ "io/fs"
"net/http"
- "path"
"strings"
"time"
"github.com/jackc/pgconn"
"github.com/jackc/pgx/v4"
- "github.com/julienschmidt/httprouter"
"go.opencensus.io/plugin/ochttp"
"go.opencensus.io/stats"
"go.opencensus.io/stats/view"
@@ -54,92 +51,34 @@
},
}
-// metricsRouter wraps an *httprouter.Router with telemetry.
+// metricsRouter wraps an *http.ServeMux with telemetry.
type metricsRouter struct {
- router *httprouter.Router
+ mux *http.ServeMux
}
-// GET is shorthand for Handle(http.MethodGet, path, handle)
-func (r *metricsRouter) GET(path string, handle httprouter.Handle) {
- r.Handle(http.MethodGet, path, handle)
+// Handle is like (*http.ServeMux).Handle but with additional metrics reporting.
+func (r *metricsRouter) Handle(pattern string, handler http.Handler) {
+ r.mux.Handle(pattern, ochttp.WithRouteTag(handler, pattern))
}
-// HEAD is shorthand for Handle(http.MethodHead, path, handle)
-func (r *metricsRouter) HEAD(path string, handle httprouter.Handle) {
- r.Handle(http.MethodHead, path, handle)
+// HandleFunc is like (*http.ServeMux).HandleFunc but with additional metrics reporting.
+func (r *metricsRouter) HandleFunc(pattern string, handler http.HandlerFunc) {
+ r.Handle(pattern, handler)
}
-// OPTIONS is shorthand for Handle(http.MethodOptions, path, handle)
-func (r *metricsRouter) OPTIONS(path string, handle httprouter.Handle) {
- r.Handle(http.MethodOptions, path, handle)
-}
-
-// POST is shorthand for Handle(http.MethodPost, path, handle)
-func (r *metricsRouter) POST(path string, handle httprouter.Handle) {
- r.Handle(http.MethodPost, path, handle)
-}
-
-// PUT is shorthand for Handle(http.MethodPut, path, handle)
-func (r *metricsRouter) PUT(path string, handle httprouter.Handle) {
- r.Handle(http.MethodPut, path, handle)
-}
-
-// PATCH is shorthand for Handle(http.MethodPatch, path, handle)
-func (r *metricsRouter) PATCH(path string, handle httprouter.Handle) {
- r.Handle(http.MethodPatch, path, handle)
-}
-
-// DELETE is shorthand for Handle(http.MethodDelete, path, handle)
-func (r *metricsRouter) DELETE(path string, handle httprouter.Handle) {
- r.Handle(http.MethodDelete, path, handle)
-}
-
-// Handler wraps *httprouter.Handler with recorded metrics.
-func (r *metricsRouter) Handler(method, path string, handler http.Handler) {
- r.router.Handler(method, path, ochttp.WithRouteTag(handler, path))
-}
-
-// HandlerFunc wraps *httprouter.HandlerFunc with recorded metrics.
-func (r *metricsRouter) HandlerFunc(method, path string, handler http.HandlerFunc) {
- r.Handler(method, path, handler)
-}
-
-// ServeFiles serves files at the specified root. The provided path
-// must end in /*filepath.
-//
-// Unlike *httprouter.ServeFiles, this method sets a Content-Type and
-// Cache-Control to "no-cache, private, max-age=0". This handler
-// also does not strip the prefix of the request path.
-func (r *metricsRouter) ServeFiles(p string, root http.FileSystem) {
- if len(p) < 10 || p[len(p)-10:] != "/*filepath" {
- panic(fmt.Sprintf("p must end with /*filepath in path %q", p))
- }
-
- s := http.FileServer(root)
- r.GET(p, func(w http.ResponseWriter, req *http.Request, ps httprouter.Params) {
- w.Header().Set("Content-Type", mime.TypeByExtension(path.Ext(req.URL.Path)))
+// ServeFiles serves files at the specified root with the
+// Cache-Control header set to "no-cache, private, max-age=0".
+func (r *metricsRouter) ServeFiles(pattern string, root fs.FS) {
+ fs := http.FileServerFS(root)
+ r.mux.HandleFunc(pattern, func(w http.ResponseWriter, req *http.Request) {
w.Header().Set("Cache-Control", "no-cache, private, max-age=0")
- s.ServeHTTP(w, req)
+ fs.ServeHTTP(w, req)
})
}
-// Lookup wraps *httprouter.Lookup.
-func (r *metricsRouter) Lookup(method, path string) (httprouter.Handle, httprouter.Params, bool) {
- return r.router.Lookup(method, path)
-}
-
-// ServeHTTP wraps *httprouter.ServeHTTP.
+// ServeHTTP implements http.Handler.
func (r *metricsRouter) ServeHTTP(w http.ResponseWriter, req *http.Request) {
- r.router.ServeHTTP(w, req)
-}
-
-// Handle calls *httprouter.ServeHTTP with additional metrics reporting.
-func (r *metricsRouter) Handle(method, path string, handle httprouter.Handle) {
- r.router.Handle(method, path, func(w http.ResponseWriter, r *http.Request, params httprouter.Params) {
- ochttp.WithRouteTag(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
- handle(w, r, params)
- }), path).ServeHTTP(w, r)
- })
+ r.mux.ServeHTTP(w, req)
}
type MetricsDB struct {
diff --git a/internal/relui/web.go b/internal/relui/web.go
index c97532c..8d999d1 100644
--- a/internal/relui/web.go
+++ b/internal/relui/web.go
@@ -24,7 +24,6 @@
"github.com/google/uuid"
"github.com/jackc/pgx/v4"
- "github.com/julienschmidt/httprouter"
"golang.org/x/build/internal/access"
"golang.org/x/build/internal/criadb"
"golang.org/x/build/internal/metrics"
@@ -71,7 +70,7 @@
func NewServer(p db.PGDBTX, w *Worker, baseURL *url.URL, header SiteHeader, ms *metrics.Service, cria *criadb.AuthDatabase) *Server {
s := &Server{
db: p,
- m: &metricsRouter{router: httprouter.New()},
+ m: &metricsRouter{mux: http.NewServeMux()},
w: w,
scheduler: NewScheduler(p, w),
baseURL: baseURL,
@@ -93,16 +92,16 @@
s.templates = template.Must(template.New("").Funcs(helpers).ParseFS(templates, "templates/*.html"))
s.homeTmpl = s.mustLookup("home.html")
s.newWorkflowTmpl = s.mustLookup("new_workflow.html")
- s.m.GET("/workflows/:id", s.showWorkflowHandler)
- s.m.POST("/workflows/:id/stop", s.stopWorkflowHandler)
- s.m.POST("/workflows/:id/tasks/:name/retry", s.retryTaskHandler)
- s.m.POST("/workflows/:id/tasks/:name/approve", s.approveTaskHandler)
- s.m.POST("/schedules/:id/delete", s.deleteScheduleHandler)
- s.m.Handler(http.MethodGet, "/metrics", ms)
- s.m.Handler(http.MethodGet, "/new_workflow", http.HandlerFunc(s.newWorkflowHandler))
- s.m.Handler(http.MethodPost, "/workflows", http.HandlerFunc(s.createWorkflowHandler))
- s.m.ServeFiles("/static/*filepath", http.FS(static))
- s.m.Handler(http.MethodGet, "/", http.HandlerFunc(s.homeHandler))
+ s.m.HandleFunc("GET /workflows/{id}", s.showWorkflowHandler)
+ s.m.HandleFunc("POST /workflows/{id}/stop", s.stopWorkflowHandler)
+ s.m.HandleFunc("POST /workflows/{id}/tasks/{name}/retry", s.retryTaskHandler)
+ s.m.HandleFunc("POST /workflows/{id}/tasks/{name}/approve", s.approveTaskHandler)
+ s.m.HandleFunc("POST /schedules/{id}/delete", s.deleteScheduleHandler)
+ s.m.Handle("GET /metrics", ms)
+ s.m.HandleFunc("GET /new_workflow", s.newWorkflowHandler)
+ s.m.HandleFunc("POST /workflows", s.createWorkflowHandler)
+ s.m.ServeFiles("GET /static/", static)
+ s.m.HandleFunc("GET /$", s.homeHandler)
if baseURL != nil && baseURL.Path != "/" && baseURL.Path != "" {
nosuffix := strings.TrimSuffix(baseURL.Path, "/")
s.bm = new(http.ServeMux)
@@ -254,10 +253,10 @@
TaskLogs map[string][]db.TaskLog
}
-func (s *Server) showWorkflowHandler(w http.ResponseWriter, r *http.Request, params httprouter.Params) {
- id, err := uuid.Parse(params.ByName("id"))
+func (s *Server) showWorkflowHandler(w http.ResponseWriter, r *http.Request) {
+ id, err := uuid.Parse(r.PathValue("id"))
if err != nil {
- log.Printf("showWorkflowHandler(_, _, %v) uuid.Parse(%v): %v", params, params.ByName("id"), err)
+ log.Printf("showWorkflowHandler: uuid.Parse(%q): %v", r.PathValue("id"), err)
http.Error(w, http.StatusText(http.StatusBadRequest), http.StatusBadRequest)
return
}
@@ -444,10 +443,10 @@
http.Redirect(w, r, s.BaseLink("/workflows", id.String()), http.StatusSeeOther)
}
-func (s *Server) retryTaskHandler(w http.ResponseWriter, r *http.Request, params httprouter.Params) {
- id, err := uuid.Parse(params.ByName("id"))
+func (s *Server) retryTaskHandler(w http.ResponseWriter, r *http.Request) {
+ id, err := uuid.Parse(r.PathValue("id"))
if err != nil {
- log.Printf("retryTaskHandler(_, _, %v) uuid.Parse(%v): %v", params, params.ByName("id"), err)
+ log.Printf("retryTaskHandler: uuid.Parse(%q): %v", r.PathValue("id"), err)
http.Error(w, http.StatusText(http.StatusBadRequest), http.StatusBadRequest)
return
}
@@ -457,7 +456,7 @@
http.Error(w, http.StatusText(http.StatusNotFound), http.StatusNotFound)
return
} else if err != nil {
- log.Printf("retryTaskHandler(_, _, %v): Workflow(%d): %v", params, id, err)
+ log.Printf("retryTaskHandler: Workflow(%v): %v", id, err)
http.Error(w, http.StatusText(http.StatusInternalServerError), http.StatusInternalServerError)
return
}
@@ -470,16 +469,16 @@
// authorizedForWorkflow writes errors to w itself.
return
}
- if err := s.w.RetryTask(r.Context(), id, params.ByName("name")); err != nil {
+ if err := s.w.RetryTask(r.Context(), id, r.PathValue("name")); err != nil {
log.Printf("s.w.RetryTask(_, %q): %v", id, err)
}
http.Redirect(w, r, s.BaseLink("/workflows", id.String()), http.StatusSeeOther)
}
-func (s *Server) approveTaskHandler(w http.ResponseWriter, r *http.Request, params httprouter.Params) {
- id, err := uuid.Parse(params.ByName("id"))
+func (s *Server) approveTaskHandler(w http.ResponseWriter, r *http.Request) {
+ id, err := uuid.Parse(r.PathValue("id"))
if err != nil {
- log.Printf("approveTaskHandler(_, _, %v) uuid.Parse(%v): %v", params, params.ByName("id"), err)
+ log.Printf("approveTaskHandler: uuid.Parse(%q): %v", r.PathValue("id"), err)
http.Error(w, http.StatusText(http.StatusBadRequest), http.StatusBadRequest)
return
}
@@ -489,7 +488,7 @@
http.Error(w, http.StatusText(http.StatusNotFound), http.StatusNotFound)
return
} else if err != nil {
- log.Printf("approveTaskHandler(_, _, %v): Workflow(%d): %v", params, id, err)
+ log.Printf("approveTaskHandler: Workflow(%v): %v", id, err)
http.Error(w, http.StatusText(http.StatusInternalServerError), http.StatusInternalServerError)
return
}
@@ -504,7 +503,7 @@
}
t, err := q.ApproveTask(r.Context(), db.ApproveTaskParams{
WorkflowID: id,
- Name: params.ByName("name"),
+ Name: r.PathValue("name"),
ApprovedAt: sql.NullTime{Time: time.Now(), Valid: true},
})
if errors.Is(err, sql.ErrNoRows) || errors.Is(err, pgx.ErrNoRows) {
@@ -519,10 +518,10 @@
http.Redirect(w, r, s.BaseLink("/workflows", id.String()), http.StatusSeeOther)
}
-func (s *Server) stopWorkflowHandler(w http.ResponseWriter, r *http.Request, params httprouter.Params) {
- id, err := uuid.Parse(params.ByName("id"))
+func (s *Server) stopWorkflowHandler(w http.ResponseWriter, r *http.Request) {
+ id, err := uuid.Parse(r.PathValue("id"))
if err != nil {
- log.Printf("stopWorkflowHandler(_, _, %v) uuid.Parse(%v): %v", params, params.ByName("id"), err)
+ log.Printf("stopWorkflowHandler: uuid.Parse(%q): %v", r.PathValue("id"), err)
http.Error(w, http.StatusText(http.StatusBadRequest), http.StatusBadRequest)
return
}
@@ -532,7 +531,7 @@
http.Error(w, http.StatusText(http.StatusNotFound), http.StatusNotFound)
return
} else if err != nil {
- log.Printf("stopWorkflowHandler(_, _, %v): Workflow(%d): %v", params, id, err)
+ log.Printf("stopWorkflowHandler: Workflow(%v): %v", id, err)
http.Error(w, http.StatusText(http.StatusInternalServerError), http.StatusInternalServerError)
return
}
@@ -552,10 +551,10 @@
http.Redirect(w, r, s.BaseLink("/"), http.StatusSeeOther)
}
-func (s *Server) deleteScheduleHandler(w http.ResponseWriter, r *http.Request, params httprouter.Params) {
- id, err := strconv.Atoi(params.ByName("id"))
+func (s *Server) deleteScheduleHandler(w http.ResponseWriter, r *http.Request) {
+ id, err := strconv.Atoi(r.PathValue("id"))
if err != nil {
- log.Printf("deleteScheduleHandler(_, _, %v) strconv.Atoi(%q) = %d, %v", params, params.ByName("id"), id, err)
+ log.Printf("deleteScheduleHandler: strconv.Atoi(%q): %v", r.PathValue("id"), err)
http.Error(w, http.StatusText(http.StatusBadRequest), http.StatusBadRequest)
return
}
@@ -590,7 +589,7 @@
http.Error(w, http.StatusText(http.StatusNotFound), http.StatusNotFound)
return
} else if err != nil {
- log.Printf("deleteScheduleHandler(_, _, %v) s.scheduler.Delete(_, %d) = %v", params, id, err)
+ log.Printf("deleteScheduleHandler: s.scheduler.Delete(_, %v) = %v", id, err)
http.Error(w, http.StatusText(http.StatusInternalServerError), http.StatusInternalServerError)
return
}
diff --git a/internal/relui/web_test.go b/internal/relui/web_test.go
index 293608a..e5bc40d 100644
--- a/internal/relui/web_test.go
+++ b/internal/relui/web_test.go
@@ -28,7 +28,6 @@
"github.com/google/uuid"
"github.com/jackc/pgx/v4"
"github.com/jackc/pgx/v4/pgxpool"
- "github.com/julienschmidt/httprouter"
"golang.org/x/build/internal/access"
"golang.org/x/build/internal/criadb"
"golang.org/x/build/internal/releasetargets"
@@ -76,8 +75,8 @@
req := httptest.NewRequest(http.MethodGet, c.path, nil)
w := httptest.NewRecorder()
- m := &metricsRouter{router: httprouter.New()}
- m.ServeFiles("/*filepath", http.FS(testStatic))
+ m := &metricsRouter{mux: http.NewServeMux()}
+ m.ServeFiles("GET /", testStatic)
m.ServeHTTP(w, req)
resp := w.Result()
defer resp.Body.Close()
@@ -895,13 +894,14 @@
t.Fatalf("FailUnfinishedTasks(_, %v) = _, %v, wanted no error", fail, err)
}
- params := httprouter.Params{{Key: "id", Value: wfID.String()}, {Key: "name", Value: "beep"}}
req := httptest.NewRequest(http.MethodPost, path.Join("/workflows/", wfID.String(), "tasks", "beep", "retry"), nil)
+ req.SetPathValue("id", wfID.String())
+ req.SetPathValue("name", "beep")
req.Header.Set("Content-Type", "application/x-www-form-urlencoded")
req = req.WithContext(access.ContextWithIAP(req.Context(), iap))
rec := httptest.NewRecorder()
- s.retryTaskHandler(rec, req, params)
+ s.retryTaskHandler(rec, req)
resp := rec.Result()
if resp.StatusCode != expectedStatus {
@@ -936,13 +936,14 @@
t.Fatalf("CreateTask(_, %v) = _, %v, wanted no error", gtg, err)
}
- params := httprouter.Params{{Key: "id", Value: wfID.String()}, {Key: "name", Value: "approve"}}
req := httptest.NewRequest(http.MethodPost, path.Join("/workflows/", wfID.String(), "tasks", "approve", "approve"), nil)
+ req.SetPathValue("id", wfID.String())
+ req.SetPathValue("name", "approve")
req.Header.Set("Content-Type", "application/x-www-form-urlencoded")
req = req.WithContext(access.ContextWithIAP(req.Context(), iap))
rec := httptest.NewRecorder()
- s.approveTaskHandler(rec, req, params)
+ s.approveTaskHandler(rec, req)
resp := rec.Result()
if resp.StatusCode != expectedStatus {
@@ -968,13 +969,13 @@
}
s.w.markRunning(&workflow.Workflow{ID: wfID}, func() {})
- params := httprouter.Params{{Key: "id", Value: wfID.String()}}
req := httptest.NewRequest(http.MethodPost, path.Join("/workflows/", wfID.String(), "stop"), nil)
+ req.SetPathValue("id", wfID.String())
req.Header.Set("Content-Type", "application/x-www-form-urlencoded")
req = req.WithContext(access.ContextWithIAP(req.Context(), iap))
rec := httptest.NewRecorder()
- s.stopWorkflowHandler(rec, req, params)
+ s.stopWorkflowHandler(rec, req)
resp := rec.Result()
if resp.StatusCode != expectedStatus {
@@ -992,13 +993,13 @@
t.Fatalf("Scheduler.Create() = _, %v, wanted no error", err)
}
- params := httprouter.Params{{Key: "id", Value: strconv.Itoa(int(sched.ID))}}
req := httptest.NewRequest(http.MethodPost, path.Join("/schedules/", strconv.Itoa(int(sched.ID)), "delete"), nil)
+ req.SetPathValue("id", strconv.Itoa(int(sched.ID)))
req.Header.Set("Content-Type", "application/x-www-form-urlencoded")
req = req.WithContext(access.ContextWithIAP(req.Context(), iap))
rec := httptest.NewRecorder()
- s.deleteScheduleHandler(rec, req, params)
+ s.deleteScheduleHandler(rec, req)
resp := rec.Result()
if resp.StatusCode != expectedStatus {