From e700f90c47c43a8df2556010096ff943e4b248c6 Mon Sep 17 00:00:00 2001 From: Denozordec Date: Wed, 8 Apr 2026 21:08:09 +0700 Subject: [PATCH] refactor: improve PATCH handling for module updates with nullable field support Enhanced the handlePatchModule function to read the request body more robustly and handle nullable fields explicitly. This allows clients to differentiate between omitted fields and fields set to null, improving data integrity during module updates. The changes streamline the JSON unmarshalling process and ensure proper handling of nullable values for community and DoH profile selections. --- internal/httpapi/routes_crud.go | 30 +++++++++++++++++++++++++++++- 1 file changed, 29 insertions(+), 1 deletion(-) diff --git a/internal/httpapi/routes_crud.go b/internal/httpapi/routes_crud.go index f818c5b..de38fd4 100644 --- a/internal/httpapi/routes_crud.go +++ b/internal/httpapi/routes_crud.go @@ -105,11 +105,39 @@ func (s *Server) handlePatchModule(w http.ResponseWriter, r *http.Request) { if !ok || !s.requireAtLeast(w, a, "editor") { return } + rawBody, err := io.ReadAll(r.Body) + if err != nil { + writeProblem(w, http.StatusBadRequest, "Bad Request", "invalid body") + return + } var body store.ModulePatch - if err := json.NewDecoder(r.Body).Decode(&body); err != nil { + if err := json.Unmarshal(rawBody, &body); err != nil { writeProblem(w, http.StatusBadRequest, "Bad Request", "invalid json") return } + // NOTE: + // In Go, unmarshalling JSON `null` into pointer fields results in nil, + // which is indistinguishable from "field omitted". For PATCH we need to + // distinguish these cases so clients can explicitly clear nullable fields. + var raw map[string]json.RawMessage + if err := json.Unmarshal(rawBody, &raw); err == nil { + if v, ok := raw["default_community_id"]; ok && string(v) == "null" { + empty := "" + body.DefaultCommunityID = &empty + } + if v, ok := raw["doh_profile_id"]; ok && string(v) == "null" { + empty := "" + body.DohProfileID = &empty + } + if v, ok := raw["cron_expr"]; ok && string(v) == "null" { + empty := "" + body.CronExpr = &empty + } + if v, ok := raw["refresh_interval_sec"]; ok && string(v) == "null" { + zero := 0 + body.RefreshIntervalSec = &zero + } + } mod, err := s.store.UpdateModule(a.TenantID, r.PathValue("module_id"), &body) if err != nil { writeStoreErr(w, err)