aboutsummaryrefslogtreecommitdiffstats
path: root/dashboard/app
diff options
context:
space:
mode:
authorDmitry Vyukov <dvyukov@google.com>2025-12-19 12:20:56 +0100
committerAleksandr Nogikh <nogikh@google.com>2025-12-22 02:13:00 +0000
commit8fb7048c5117ccb592deb5e8e4a62027e6d399cf (patch)
tree08c26b5b08d7fc70f7d2d067dcc3fec7d17b537b /dashboard/app
parente14dbeb99dc80832252033b9ed81b85b13bef5c4 (diff)
dashboard/app: add typed handler middleware
Remove duplicated code related to request deserialization using middleware.
Diffstat (limited to 'dashboard/app')
-rw-r--r--dashboard/app/api.go174
-rw-r--r--dashboard/app/reporting_external.go35
2 files changed, 59 insertions, 150 deletions
diff --git a/dashboard/app/api.go b/dashboard/app/api.go
index 49c524c14..1e7d655c4 100644
--- a/dashboard/app/api.go
+++ b/dashboard/app/api.go
@@ -46,20 +46,20 @@ func initAPIHandlers() {
}
var apiHandlers = map[string]APIHandler{
- "log_error": apiLogError,
- "job_poll": apiJobPoll,
- "job_reset": apiJobReset,
- "job_done": apiJobDone,
- "reporting_poll_bugs": apiReportingPollBugs,
- "reporting_poll_notifs": apiReportingPollNotifications,
- "reporting_poll_closed": apiReportingPollClosed,
- "reporting_update": apiReportingUpdate,
- "new_test_job": apiNewTestJob,
- "needed_assets": apiNeededAssetsList,
- "load_full_bug": apiLoadFullBug,
- "save_discussion": apiSaveDiscussion,
- "create_upload_url": apiCreateUploadURL,
- "send_email": apiSendEmail,
+ "log_error": typedHandler(apiLogError),
+ "job_poll": typedHandler(apiJobPoll),
+ "job_reset": typedHandler(apiJobReset),
+ "job_done": typedHandler(apiJobDone),
+ "reporting_poll_bugs": typedHandler(apiReportingPollBugs),
+ "reporting_poll_notifs": typedHandler(apiReportingPollNotifications),
+ "reporting_poll_closed": typedHandler(apiReportingPollClosed),
+ "reporting_update": typedHandler(apiReportingUpdate),
+ "new_test_job": typedHandler(apiNewTestJob),
+ "needed_assets": typedHandler(apiNeededAssetsList),
+ "load_full_bug": typedHandler(apiLoadFullBug),
+ "save_discussion": typedHandler(apiSaveDiscussion),
+ "create_upload_url": typedHandler(apiCreateUploadURL),
+ "send_email": typedHandler(apiSendEmail),
"save_coverage": gcsPayloadHandler(apiSaveCoverage),
"upload_build": nsHandler(apiUploadBuild),
"builder_poll": nsHandler(apiBuilderPoll),
@@ -79,7 +79,6 @@ var apiHandlers = map[string]APIHandler{
type JSONHandler func(c context.Context, r *http.Request) (interface{}, error)
type APIHandler func(c context.Context, payload io.Reader) (interface{}, error)
-type APINamespaceHandler func(c context.Context, ns string, payload io.Reader) (interface{}, error)
const (
maxReproPerBug = 10
@@ -206,30 +205,34 @@ func gcsPayloadHandler(handler APIHandler) APIHandler {
}
}
-func nsHandler(handler APINamespaceHandler) APIHandler {
- return func(c context.Context, payload io.Reader) (interface{}, error) {
- ns := contextNamespace(c)
+func nsHandler[Req any](handler func(context.Context, string, *Req) (any, error)) APIHandler {
+ return typedHandler(func(ctx context.Context, req *Req) (any, error) {
+ ns := contextNamespace(ctx)
if ns == "" {
return nil, fmt.Errorf("must be called within a namespace")
}
- return handler(c, ns, payload)
- }
+ return handler(ctx, ns, req)
+ })
}
-func apiLogError(c context.Context, payload io.Reader) (interface{}, error) {
- req := new(dashapi.LogEntry)
- if err := json.NewDecoder(payload).Decode(req); err != nil {
- return nil, fmt.Errorf("failed to unmarshal request: %w", err)
+func typedHandler[Req any](handler func(context.Context, *Req) (any, error)) APIHandler {
+ return func(ctx context.Context, payload io.Reader) (interface{}, error) {
+ req := new(Req)
+ if payload != nil {
+ if err := json.NewDecoder(payload).Decode(req); err != nil {
+ return nil, fmt.Errorf("failed to unmarshal request %T: %w", req, err)
+ }
+ }
+ return handler(ctx, req)
}
+}
+
+func apiLogError(c context.Context, req *dashapi.LogEntry) (interface{}, error) {
log.Errorf(c, "%v: %v", req.Name, req.Text)
return nil, nil
}
-func apiBuilderPoll(c context.Context, ns string, payload io.Reader) (interface{}, error) {
- req := new(dashapi.BuilderPollReq)
- if err := json.NewDecoder(payload).Decode(req); err != nil {
- return nil, fmt.Errorf("failed to unmarshal request: %w", err)
- }
+func apiBuilderPoll(c context.Context, ns string, req *dashapi.BuilderPollReq) (interface{}, error) {
bugs, _, err := loadAllBugs(c, func(query *db.Query) *db.Query {
return query.Filter("Namespace=", ns).
Filter("Status<", BugStatusFixed)
@@ -274,7 +277,7 @@ func reportEmail(c context.Context, ns string) string {
return ""
}
-func apiCommitPoll(c context.Context, ns string, payload io.Reader) (interface{}, error) {
+func apiCommitPoll(c context.Context, ns string, req *any) (interface{}, error) {
resp := &dashapi.CommitPollResp{
ReportEmail: reportEmail(c, ns),
}
@@ -340,11 +343,7 @@ func pollBackportCommits(c context.Context, ns string, count int) ([]string, err
return backportTitles, nil
}
-func apiUploadCommits(c context.Context, ns string, payload io.Reader) (interface{}, error) {
- req := new(dashapi.CommitPollResultReq)
- if err := json.NewDecoder(payload).Decode(req); err != nil {
- return nil, fmt.Errorf("failed to unmarshal request: %w", err)
- }
+func apiUploadCommits(c context.Context, ns string, req *dashapi.CommitPollResultReq) (interface{}, error) {
// This adds fixing commits to bugs.
err := addCommitsToBugs(c, ns, "", nil, req.Commits)
if err != nil {
@@ -445,46 +444,28 @@ func addCommitInfoToBugImpl(c context.Context, bug *Bug, com dashapi.Commit) (bo
return changed, nil
}
-func apiJobPoll(c context.Context, payload io.Reader) (interface{}, error) {
+func apiJobPoll(c context.Context, req *dashapi.JobPollReq) (interface{}, error) {
if stop, err := emergentlyStopped(c); err != nil || stop {
// The bot's operation was aborted. Don't accept new crash reports.
return &dashapi.JobPollResp{}, err
}
- req := new(dashapi.JobPollReq)
- if err := json.NewDecoder(payload).Decode(req); err != nil {
- return nil, fmt.Errorf("failed to unmarshal request: %w", err)
- }
if len(req.Managers) == 0 {
return nil, fmt.Errorf("no managers")
}
return pollPendingJobs(c, req.Managers)
}
-// nolint: dupl
-func apiJobDone(c context.Context, payload io.Reader) (interface{}, error) {
- req := new(dashapi.JobDoneReq)
- if err := json.NewDecoder(payload).Decode(req); err != nil {
- return nil, fmt.Errorf("failed to unmarshal request: %w", err)
- }
+func apiJobDone(c context.Context, req *dashapi.JobDoneReq) (interface{}, error) {
err := doneJob(c, req)
return nil, err
}
-// nolint: dupl
-func apiJobReset(c context.Context, payload io.Reader) (interface{}, error) {
- req := new(dashapi.JobResetReq)
- if err := json.NewDecoder(payload).Decode(req); err != nil {
- return nil, fmt.Errorf("failed to unmarshal request: %w", err)
- }
+func apiJobReset(c context.Context, req *dashapi.JobResetReq) (interface{}, error) {
err := resetJobs(c, req)
return nil, err
}
-func apiUploadBuild(c context.Context, ns string, payload io.Reader) (interface{}, error) {
- req := new(dashapi.Build)
- if err := json.NewDecoder(payload).Decode(req); err != nil {
- return nil, fmt.Errorf("failed to unmarshal request: %w", err)
- }
+func apiUploadBuild(c context.Context, ns string, req *dashapi.Build) (interface{}, error) {
now := timeNow(c)
_, isNewBuild, err := uploadBuild(c, now, ns, req, BuildNormal)
if err != nil {
@@ -751,11 +732,7 @@ func managerList(c context.Context, ns string) ([]string, error) {
return managers, nil
}
-func apiReportBuildError(c context.Context, ns string, payload io.Reader) (interface{}, error) {
- req := new(dashapi.BuildErrorReq)
- if err := json.NewDecoder(payload).Decode(req); err != nil {
- return nil, fmt.Errorf("failed to unmarshal request: %w", err)
- }
+func apiReportBuildError(c context.Context, ns string, req *dashapi.BuildErrorReq) (interface{}, error) {
now := timeNow(c)
build, _, err := uploadBuild(c, now, ns, &req.Build, BuildFailed)
if err != nil {
@@ -785,15 +762,11 @@ const (
suppressedReportTitle = "suppressed report"
)
-func apiReportCrash(c context.Context, ns string, payload io.Reader) (interface{}, error) {
+func apiReportCrash(c context.Context, ns string, req *dashapi.Crash) (interface{}, error) {
if stop, err := emergentlyStopped(c); err != nil || stop {
// The bot's operation was aborted. Don't accept new crash reports.
return &dashapi.ReportCrashResp{}, err
}
- req := new(dashapi.Crash)
- if err := json.NewDecoder(payload).Decode(req); err != nil {
- return nil, fmt.Errorf("failed to unmarshal request: %w", err)
- }
build, err := loadBuild(c, ns, req.BuildID)
if err != nil {
return nil, err
@@ -1094,13 +1067,8 @@ func purgeOldCrashes(c context.Context, bug *Bug, bugKey *db.Key) {
log.Infof(c, "deleted %v crashes for bug %q", deleted, bug.Title)
}
-func apiReportFailedRepro(c context.Context, ns string, payload io.Reader) (interface{}, error) {
- req := new(dashapi.CrashID)
- if err := json.NewDecoder(payload).Decode(req); err != nil {
- return nil, fmt.Errorf("failed to unmarshal request: %w", err)
- }
+func apiReportFailedRepro(c context.Context, ns string, req *dashapi.CrashID) (interface{}, error) {
req.Title = canonicalizeCrashTitle(req.Title, req.Corrupted, req.Suppressed)
-
bug, err := findExistingBugForCrash(c, ns, []string{req.Title})
if err != nil {
return nil, err
@@ -1166,11 +1134,7 @@ func saveReproAttempt(c context.Context, bug *Bug, build *Build, log []byte) err
return nil
}
-func apiNeedRepro(c context.Context, ns string, payload io.Reader) (interface{}, error) {
- req := new(dashapi.CrashID)
- if err := json.NewDecoder(payload).Decode(req); err != nil {
- return nil, fmt.Errorf("failed to unmarshal request: %w", err)
- }
+func apiNeedRepro(c context.Context, ns string, req *dashapi.CrashID) (interface{}, error) {
if req.Corrupted {
resp := &dashapi.NeedReproResp{
NeedRepro: false,
@@ -1218,11 +1182,7 @@ func normalizeCrashTitle(title string) string {
return strings.TrimSpace(limitLength(title, maxTextLen))
}
-func apiManagerStats(c context.Context, ns string, payload io.Reader) (interface{}, error) {
- req := new(dashapi.ManagerStatsReq)
- if err := json.NewDecoder(payload).Decode(req); err != nil {
- return nil, fmt.Errorf("failed to unmarshal request: %w", err)
- }
+func apiManagerStats(c context.Context, ns string, req *dashapi.ManagerStatsReq) (interface{}, error) {
now := timeNow(c)
err := updateManager(c, ns, req.Name, func(mgr *Manager, stats *ManagerStats) error {
mgr.Link = req.Addr
@@ -1243,11 +1203,7 @@ func apiManagerStats(c context.Context, ns string, payload io.Reader) (interface
return nil, err
}
-func apiUpdateReport(c context.Context, ns string, payload io.Reader) (interface{}, error) {
- req := new(dashapi.UpdateReportReq)
- if err := json.NewDecoder(payload).Decode(req); err != nil {
- return nil, fmt.Errorf("failed to unmarshal request: %w", err)
- }
+func apiUpdateReport(c context.Context, ns string, req *dashapi.UpdateReportReq) (interface{}, error) {
bug := new(Bug)
bugKey := db.NewKey(c, "Bug", req.BugID, 0, nil)
if err := db.Get(c, bugKey, bug); err != nil {
@@ -1273,7 +1229,7 @@ func apiUpdateReport(c context.Context, ns string, payload io.Reader) (interface
return nil, runInTransaction(c, tx, nil)
}
-func apiBugList(c context.Context, ns string, payload io.Reader) (interface{}, error) {
+func apiBugList(c context.Context, ns string, req *any) (interface{}, error) {
keys, err := db.NewQuery("Bug").
Filter("Namespace=", ns).
KeysOnly().
@@ -1288,11 +1244,7 @@ func apiBugList(c context.Context, ns string, payload io.Reader) (interface{}, e
return resp, nil
}
-func apiLoadBug(c context.Context, ns string, payload io.Reader) (interface{}, error) {
- req := new(dashapi.LoadBugReq)
- if err := json.NewDecoder(payload).Decode(req); err != nil {
- return nil, fmt.Errorf("failed to unmarshal request: %w", err)
- }
+func apiLoadBug(c context.Context, ns string, req *dashapi.LoadBugReq) (interface{}, error) {
bug := new(Bug)
bugKey := db.NewKey(c, "Bug", req.ID, 0, nil)
if err := db.Get(c, bugKey, bug); err != nil {
@@ -1304,11 +1256,7 @@ func apiLoadBug(c context.Context, ns string, payload io.Reader) (interface{}, e
return loadBugReport(c, bug)
}
-func apiLoadFullBug(c context.Context, payload io.Reader) (interface{}, error) {
- req := new(dashapi.LoadFullBugReq)
- if err := json.NewDecoder(payload).Decode(req); err != nil {
- return nil, fmt.Errorf("failed to unmarshal request: %w", err)
- }
+func apiLoadFullBug(c context.Context, req *dashapi.LoadFullBugReq) (interface{}, error) {
bug, bugKey, err := findBugByReportingID(c, req.BugID)
if err != nil {
return nil, fmt.Errorf("failed to find the bug: %w", err)
@@ -1334,11 +1282,7 @@ func loadBugReport(c context.Context, bug *Bug) (*dashapi.BugReport, error) {
return createBugReport(c, bug, crash, crashKey, bugReporting, reporting)
}
-func apiAddBuildAssets(c context.Context, ns string, payload io.Reader) (interface{}, error) {
- req := new(dashapi.AddBuildAssetsReq)
- if err := json.NewDecoder(payload).Decode(req); err != nil {
- return nil, fmt.Errorf("failed to unmarshal request: %w", err)
- }
+func apiAddBuildAssets(c context.Context, ns string, req *dashapi.AddBuildAssetsReq) (interface{}, error) {
assets := []Asset{}
for i, toAdd := range req.Assets {
asset, err := parseIncomingAsset(c, toAdd, ns)
@@ -1379,7 +1323,7 @@ func parseIncomingAsset(c context.Context, newAsset dashapi.NewAsset, ns string)
}, nil
}
-func apiNeededAssetsList(c context.Context, payload io.Reader) (interface{}, error) {
+func apiNeededAssetsList(c context.Context, req *any) (interface{}, error) {
return queryNeededAssets(c)
}
@@ -1760,11 +1704,7 @@ func handleRefreshSubsystems(w http.ResponseWriter, r *http.Request) {
}
}
-func apiSaveDiscussion(c context.Context, payload io.Reader) (interface{}, error) {
- req := new(dashapi.SaveDiscussionReq)
- if err := json.NewDecoder(payload).Decode(req); err != nil {
- return nil, fmt.Errorf("failed to unmarshal request: %w", err)
- }
+func apiSaveDiscussion(c context.Context, req *dashapi.SaveDiscussionReq) (interface{}, error) {
d := req.Discussion
newBugIDs := []string{}
for _, id := range d.BugIDs {
@@ -1803,11 +1743,7 @@ func recordEmergencyStop(c context.Context) error {
// Share crash logs for non-reproduced bugs with syz-managers.
// In future, this can also take care of repro exchange between instances
// in the place of syz-hub.
-func apiLogToReproduce(c context.Context, ns string, payload io.Reader) (interface{}, error) {
- req := new(dashapi.LogToReproReq)
- if err := json.NewDecoder(payload).Decode(req); err != nil {
- return nil, fmt.Errorf("failed to unmarshal request: %w", err)
- }
+func apiLogToReproduce(c context.Context, ns string, req *dashapi.LogToReproReq) (interface{}, error) {
build, err := loadBuild(c, ns, req.BuildID)
if err != nil {
return nil, err
@@ -1927,7 +1863,7 @@ func takeReproTask(c context.Context, ns, manager string) ([]byte, error) {
return log, err
}
-func apiCreateUploadURL(c context.Context, payload io.Reader) (interface{}, error) {
+func apiCreateUploadURL(c context.Context, req *any) (interface{}, error) {
bucket := getConfig(c).UploadBucket
if bucket == "" {
return nil, errors.New("not configured")
@@ -1935,11 +1871,7 @@ func apiCreateUploadURL(c context.Context, payload io.Reader) (interface{}, erro
return fmt.Sprintf("%s/%s.upload", bucket, uuid.New().String()), nil
}
-func apiSendEmail(c context.Context, payload io.Reader) (interface{}, error) {
- req := new(dashapi.SendEmailReq)
- if err := json.NewDecoder(payload).Decode(req); err != nil {
- return nil, fmt.Errorf("failed to unmarshal request: %w", err)
- }
+func apiSendEmail(c context.Context, req *dashapi.SendEmailReq) (interface{}, error) {
var headers mail.Header
if req.InReplyTo != "" {
headers = mail.Header{"In-Reply-To": []string{req.InReplyTo}}
diff --git a/dashboard/app/reporting_external.go b/dashboard/app/reporting_external.go
index d603a7107..8dd4eb444 100644
--- a/dashboard/app/reporting_external.go
+++ b/dashboard/app/reporting_external.go
@@ -5,10 +5,7 @@ package main
import (
"context"
- "encoding/json"
"errors"
- "fmt"
- "io"
"github.com/google/syzkaller/dashboard/dashapi"
"google.golang.org/appengine/v2/log"
@@ -18,14 +15,10 @@ import (
// The external system is meant to poll for new bugs with apiReportingPoll,
// and report back bug status updates with apiReportingUpdate.
-func apiReportingPollBugs(c context.Context, payload io.Reader) (interface{}, error) {
+func apiReportingPollBugs(c context.Context, req *dashapi.PollBugsRequest) (interface{}, error) {
if stop, err := emergentlyStopped(c); err != nil || stop {
return &dashapi.PollBugsResponse{}, err
}
- req := new(dashapi.PollBugsRequest)
- if err := json.NewDecoder(payload).Decode(req); err != nil {
- return nil, fmt.Errorf("failed to unmarshal request: %w", err)
- }
reports := reportingPollBugs(c, req.Type)
resp := &dashapi.PollBugsResponse{
Reports: reports,
@@ -38,15 +31,11 @@ func apiReportingPollBugs(c context.Context, payload io.Reader) (interface{}, er
return resp, nil
}
-func apiReportingPollNotifications(c context.Context, payload io.Reader,
-) (interface{}, error) {
+func apiReportingPollNotifications(c context.Context, req *dashapi.PollNotificationsRequest) (
+ interface{}, error) {
if stop, err := emergentlyStopped(c); err != nil || stop {
return &dashapi.PollNotificationsResponse{}, err
}
- req := new(dashapi.PollNotificationsRequest)
- if err := json.NewDecoder(payload).Decode(req); err != nil {
- return nil, fmt.Errorf("failed to unmarshal request: %w", err)
- }
notifs := reportingPollNotifications(c, req.Type)
resp := &dashapi.PollNotificationsResponse{
Notifications: notifs,
@@ -54,14 +43,10 @@ func apiReportingPollNotifications(c context.Context, payload io.Reader,
return resp, nil
}
-func apiReportingPollClosed(c context.Context, payload io.Reader) (interface{}, error) {
+func apiReportingPollClosed(c context.Context, req *dashapi.PollClosedRequest) (interface{}, error) {
if stop, err := emergentlyStopped(c); err != nil || stop {
return &dashapi.PollClosedResponse{}, err
}
- req := new(dashapi.PollClosedRequest)
- if err := json.NewDecoder(payload).Decode(req); err != nil {
- return nil, fmt.Errorf("failed to unmarshal request: %w", err)
- }
ids, err := reportingPollClosed(c, req.IDs)
if err != nil {
return nil, err
@@ -72,11 +57,7 @@ func apiReportingPollClosed(c context.Context, payload io.Reader) (interface{},
return resp, nil
}
-func apiReportingUpdate(c context.Context, payload io.Reader) (interface{}, error) {
- req := new(dashapi.BugUpdate)
- if err := json.NewDecoder(payload).Decode(req); err != nil {
- return nil, fmt.Errorf("failed to unmarshal request: %w", err)
- }
+func apiReportingUpdate(c context.Context, req *dashapi.BugUpdate) (interface{}, error) {
if req.JobID != "" {
resp := &dashapi.BugUpdateReply{
OK: true,
@@ -97,11 +78,7 @@ func apiReportingUpdate(c context.Context, payload io.Reader) (interface{}, erro
}, nil
}
-func apiNewTestJob(c context.Context, payload io.Reader) (interface{}, error) {
- req := new(dashapi.TestPatchRequest)
- if err := json.NewDecoder(payload).Decode(req); err != nil {
- return nil, fmt.Errorf("failed to unmarshal request: %w", err)
- }
+func apiNewTestJob(c context.Context, req *dashapi.TestPatchRequest) (interface{}, error) {
resp := &dashapi.TestPatchReply{}
err := handleExternalTestRequest(c, req)
if err != nil {