From 98cc15b30777f52bb9a21f72ee3abb24fe094363 Mon Sep 17 00:00:00 2001 From: bircni Date: Sun, 14 Jun 2026 20:06:04 +0200 Subject: [PATCH] fix(api): nil pointer panic when filtering tracked times by a non-existent user (#38112) (#38115) Backport #38112 ## Problem `GET /repos/{owner}/{repo}/times` and `GET /repos/{owner}/{repo}/issues/{index}/times` crash with a nil pointer dereference when the `user` query filter names a user that does not exist. ## Root cause In `ListTrackedTimes` and `ListTrackedTimesByRepository`, the `IsErrUserNotExist` branch sends the 404 but is missing a `return`, so execution falls through to `opts.UserID = user.ID` with a nil `user`. --------- Co-authored-by: Pycub --- routers/api/v1/repo/issue_tracked_time.go | 6 ++- .../api_issue_tracked_time_test.go | 39 +++++++++++++++++++ 2 files changed, 43 insertions(+), 2 deletions(-) diff --git a/routers/api/v1/repo/issue_tracked_time.go b/routers/api/v1/repo/issue_tracked_time.go index 7c1e77ccf5c..f429e40d6b7 100644 --- a/routers/api/v1/repo/issue_tracked_time.go +++ b/routers/api/v1/repo/issue_tracked_time.go @@ -95,7 +95,8 @@ func ListTrackedTimes(ctx *context.APIContext) { if qUser != "" { user, err := user_model.GetUserByName(ctx, qUser) if user_model.IsErrUserNotExist(err) { - ctx.APIError(http.StatusNotFound, err) + ctx.APIError(http.StatusNotFound, err.Error()) + return } else if err != nil { ctx.APIErrorInternal(err) return @@ -523,7 +524,8 @@ func ListTrackedTimesByRepository(ctx *context.APIContext) { if qUser != "" { user, err := user_model.GetUserByName(ctx, qUser) if user_model.IsErrUserNotExist(err) { - ctx.APIError(http.StatusNotFound, err) + ctx.APIError(http.StatusNotFound, err.Error()) + return } else if err != nil { ctx.APIErrorInternal(err) return diff --git a/tests/integration/api_issue_tracked_time_test.go b/tests/integration/api_issue_tracked_time_test.go index 12f4def9e1a..e5d0c1c9a78 100644 --- a/tests/integration/api_issue_tracked_time_test.go +++ b/tests/integration/api_issue_tracked_time_test.go @@ -13,6 +13,7 @@ import ( issues_model "code.gitea.io/gitea/models/issues" "code.gitea.io/gitea/models/unittest" user_model "code.gitea.io/gitea/models/user" + "code.gitea.io/gitea/modules/json" api "code.gitea.io/gitea/modules/structs" "code.gitea.io/gitea/tests" @@ -63,6 +64,44 @@ func TestAPIGetTrackedTimes(t *testing.T) { assert.Equal(t, int64(6), filterAPITimes[1].ID) } +// TestAPIGetTrackedTimesNonExistentUserFilter ensures filtering by a user that +// does not exist returns a clean 404 instead of panicking (nil pointer dereference). +func TestAPIGetTrackedTimesNonExistentUserFilter(t *testing.T) { + defer tests.PrepareTestEnv(t)() + + user2 := unittest.AssertExistsAndLoadBean(t, &user_model.User{ID: 2}) + issue2 := unittest.AssertExistsAndLoadBean(t, &issues_model.Issue{ID: 2}) + assert.NoError(t, issue2.LoadRepo(t.Context())) + + session := loginUser(t, user2.Name) + token := getTokenForLoggedInUser(t, session, auth_model.AccessTokenScopeReadIssue, auth_model.AccessTokenScopeReadRepository) + + for _, tc := range []struct { + name string + url string + }{ + {"repository level", fmt.Sprintf("/api/v1/repos/%s/%s/times?user=nonexistentuser", user2.Name, issue2.Repo.Name)}, + {"issue level", fmt.Sprintf("/api/v1/repos/%s/%s/issues/%d/times?user=nonexistentuser", user2.Name, issue2.Repo.Name, issue2.Index)}, + } { + t.Run(tc.name, func(t *testing.T) { + req := NewRequest(t, "GET", tc.url).AddTokenAuth(token) + resp := MakeRequest(t, req, http.StatusNotFound) + + assert.True(t, json.Valid(resp.Body.Bytes()), "response body must be a single JSON value, got: %s", resp.Body.Bytes()) + + var apiError api.APIError + DecodeJSON(t, resp, &apiError) + assert.Contains(t, apiError.Message, "user does not exist") + }) + } + + t.Run("existing user", func(t *testing.T) { + req := NewRequestf(t, "GET", "/api/v1/repos/%s/%s/times?user=%s", user2.Name, issue2.Repo.Name, user2.Name).AddTokenAuth(token) + resp := MakeRequest(t, req, http.StatusOK) + DecodeJSON(t, resp, api.TrackedTimeList{}) + }) +} + func TestAPIDeleteTrackedTime(t *testing.T) { defer tests.PrepareTestEnv(t)()