From 5aea433c716584477f52f3aec2cf8963a3e5ded6 Mon Sep 17 00:00:00 2001 From: silverwind Date: Wed, 4 Nov 2026 07:08:08 +0100 Subject: [PATCH] fix(api): don't panic on null milestone due_on (#40171) On release/v28, `PATCH /repos/{owner}/{repo}/milestones/{id}` with `"due_on": null`, the documented way to remove a deadline, returns HTTP 500. The backport https://github.com/go-gitea/gitea/pull/39921 changed `EditMilestoneOption.Deadline` to `optional.Option[*string]`, which decodes `null` as a slice holding a nil pointer, and the go-chi/binding validator panics on that element (`reflect: call of reflect.Value.Interface on zero Value`). The field has no binding rules, so tag it `form:"-"` to skip validation. main is unaffected since https://github.com/go-gitea/gitea/pull/39528 moved the binding into Gitea. --- modules/structs/issue_milestone.go | 2 +- tests/integration/api_issue_milestone_test.go | 8 +++++--- 2 files changed, 6 insertions(+), 4 deletions(-) diff --git a/modules/structs/issue_milestone.go b/modules/structs/issue_milestone.go index 5932ffdedcb..8acb94c3a2c 100644 --- a/modules/structs/issue_milestone.go +++ b/modules/structs/issue_milestone.go @@ -58,5 +58,5 @@ type EditMilestoneOption struct { State *string `json:"state"` // Deadline is the due date for the milestone, use null or empty string to remove the deadline // swagger:strfmt date-time - Deadline optional.Option[*string] `json:"due_on,omitzero"` + Deadline optional.Option[*string] `json:"due_on,omitzero" form:"-"` // go-chi/binding panics validating the nil element a JSON null decodes to } diff --git a/tests/integration/api_issue_milestone_test.go b/tests/integration/api_issue_milestone_test.go index cb3f87072fd..8a49a7649c6 100644 --- a/tests/integration/api_issue_milestone_test.go +++ b/tests/integration/api_issue_milestone_test.go @@ -49,9 +49,11 @@ func TestAPIIssuesMilestone(t *testing.T) { apiMilestone2 := DecodeJSON(t, resp, &structs.Milestone{}) assert.EqualValues(t, "closed", apiMilestone2.State) - req = NewRequestWithJSON(t, "PATCH", urlStr, map[string]any{"due_on": ""}).AddTokenAuth(token) - resp = MakeRequest(t, req, http.StatusOK) - assert.Nil(t, DecodeJSON(t, resp, &structs.Milestone{}).Deadline) + for _, dueOn := range []any{"", nil} { + req = NewRequestWithJSON(t, "PATCH", urlStr, map[string]any{"due_on": dueOn}).AddTokenAuth(token) + resp = MakeRequest(t, req, http.StatusOK) + assert.Nil(t, DecodeJSON(t, resp, &structs.Milestone{}).Deadline) + } req = NewRequestWithJSON(t, "POST", fmt.Sprintf("/api/v1/repos/%s/%s/milestones", owner.Name, repo.Name), structs.CreateMilestoneOption{ Title: "wow",