P8.1: fix browser inbound re-homing — nodeId probe must parse form-encoded bodies
updateInbound detected an explicit nodeId by probing the raw request body as JSON only (requestHasJSONKey). The panel frontend, however, posts application/x-www-form-urlencoded by default (http-init.ts form-encodes every POST without a declared JSON Content-Type), so the probe missed on every real browser save and UpdateInbound silently kept the stored assignment — the edit dialog's 部署到 picker never moved anything, while JSON API callers (and the P8 E2E, which posted JSON) worked. requestHasKey is now content-type aware: JSON bodies are probed as before; urlencoded bodies are parsed with url.ParseQuery, where the browser's null encoding (a bare "nodeId=") also counts as provided — it binds as *int(0) and the existing 0->nil normalisation turns it into the move-to-local target. Regression tests post the exact browser wire format through the real handler stack: form move between nodes, form post without nodeId (assignment kept), form post with nodeId= (move to local, no binding error), plus the JSON move at HTTP level. The two form-move tests fail on the unfixed code and pass with the fix.
This commit is contained in:
@@ -4,7 +4,9 @@ import (
|
||||
"bytes"
|
||||
"encoding/json"
|
||||
"io"
|
||||
"mime"
|
||||
"net"
|
||||
"net/url"
|
||||
"strconv"
|
||||
"strings"
|
||||
|
||||
@@ -224,11 +226,17 @@ func (a *InboundController) bulkDelInbounds(c *gin.Context) {
|
||||
notifyClientsChanged()
|
||||
}
|
||||
|
||||
// requestHasJSONKey reports whether the request body is a JSON object that
|
||||
// explicitly carries key — present with a null value counts. The body is
|
||||
// restored so the normal bind can read it again. Non-JSON bodies (form
|
||||
// posts) report false for every key.
|
||||
func requestHasJSONKey(c *gin.Context, key string) bool {
|
||||
// requestHasKey reports whether the request body explicitly carries key —
|
||||
// present with a JSON null or an empty form value counts. The probe must
|
||||
// understand both wire formats this endpoint receives: API clients post
|
||||
// JSON, but the panel frontend posts application/x-www-form-urlencoded by
|
||||
// default (frontend/src/api/http-init.ts form-encodes every POST that does
|
||||
// not declare a JSON Content-Type, encoding a null value as a bare "key=").
|
||||
// A JSON-only probe silently reports false for every real browser post —
|
||||
// that is exactly how browser-driven nodeId reassignments became a no-op
|
||||
// while JSON callers kept working. The body is restored so the normal
|
||||
// bind can read it again.
|
||||
func requestHasKey(c *gin.Context, key string) bool {
|
||||
if c.Request == nil || c.Request.Body == nil {
|
||||
return false
|
||||
}
|
||||
@@ -237,11 +245,27 @@ func requestHasJSONKey(c *gin.Context, key string) bool {
|
||||
return false
|
||||
}
|
||||
c.Request.Body = io.NopCloser(bytes.NewReader(body))
|
||||
var probe map[string]json.RawMessage
|
||||
if json.Unmarshal(body, &probe) != nil {
|
||||
return false
|
||||
|
||||
mediaType, _, _ := mime.ParseMediaType(c.GetHeader("Content-Type"))
|
||||
if mediaType == "application/x-www-form-urlencoded" {
|
||||
return formHasKey(body, key)
|
||||
}
|
||||
_, ok := probe[key]
|
||||
var probe map[string]json.RawMessage
|
||||
if json.Unmarshal(body, &probe) == nil {
|
||||
_, ok := probe[key]
|
||||
return ok
|
||||
}
|
||||
// Undeclared or unexpected Content-Type with a non-JSON body: fall
|
||||
// back to form parsing so the key is still understood.
|
||||
return formHasKey(body, key)
|
||||
}
|
||||
|
||||
// formHasKey reports whether a urlencoded body carries key, even with an
|
||||
// empty value. ParseQuery keeps every valid pair it finds, so its map is
|
||||
// usable even when it also returns an error for a later malformed pair.
|
||||
func formHasKey(body []byte, key string) bool {
|
||||
values, _ := url.ParseQuery(string(body))
|
||||
_, ok := values[key]
|
||||
return ok
|
||||
}
|
||||
|
||||
@@ -255,16 +279,19 @@ func (a *InboundController) updateInbound(c *gin.Context) {
|
||||
inbound := &model.Inbound{
|
||||
Id: id,
|
||||
}
|
||||
// An explicitly sent nodeId (even null) is a (re)assignment request —
|
||||
// UpdateInbound migrates the inbound when it differs from the stored
|
||||
// assignment. An omitted nodeId keeps the stored assignment, so detect
|
||||
// the key's presence on the raw body before binding consumes it.
|
||||
nodeIDProvided := requestHasJSONKey(c, "nodeId")
|
||||
// An explicitly sent nodeId (JSON null, or the form encoding of one —
|
||||
// a bare "nodeId=") is a (re)assignment request: UpdateInbound migrates
|
||||
// the inbound when it differs from the stored assignment. An omitted
|
||||
// nodeId keeps the stored assignment, so detect the key's presence on
|
||||
// the raw body before binding consumes it.
|
||||
nodeIDProvided := requestHasKey(c, "nodeId")
|
||||
if !middleware.BindAndValidateInto(c, inbound) {
|
||||
return
|
||||
}
|
||||
inbound.NodeIDProvided = nodeIDProvided
|
||||
// Same NodeID=0 → nil normalisation as addInbound.
|
||||
// Same NodeID=0 → nil normalisation as addInbound. This also covers
|
||||
// the browser's encoding of "back to local": the form post carries a
|
||||
// bare "nodeId=", which gin binds as *int(0) rather than nil.
|
||||
if inbound.NodeID != nil && *inbound.NodeID == 0 {
|
||||
inbound.NodeID = nil
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user