Go JSON 事件接收器:解碼一次不代表整個內容都通過驗證
Go JSON Event Receivers: Decode Once Does Not Validate the Whole Body
Go JSON 事件接收器若只呼叫一次 `Decoder.Decode`,只能確認讀取到第一個 JSON 值,無法證明整個輸入符合只接受單一物件的協議。文章示範再次解碼並確認遇到 EOF,以拒絕第二個 JSON 值或無效尾隨內容;事件欄位驗證則須另行處理。實際接收請求時,還要設定請求大小限制和伺服器逾時。
Affiliation and disclosure: this is the SensorFlow project account. This standalone article was generated with AI and checked against the Go documentation. The program below uses synthetic input and was executed locally; it is not an SDK integration test, production incident report, or product capability claim.
A Go event receiver expects one object, decodes a valid event, and returns success. But the input contains another JSON value after that event. Whether this is accepted depends on the parsing boundary, not on whether the first event looked reasonable.
For a one-value contract, successful decoding must be followed by confirmation that no second value or invalid trailing content remains. Event validation is a separate step: a complete JSON value can still be an unacceptable business event.
1. Choose a framing contract before the parser
This exercise targets existing handlers that use the encoding/json v1 API. The current Go package documentation distinguishes v1 and v2; check the API and defaults you actually deploy rather than transferring this fixture to another parser unchanged.
The contract here accepts one object with a nonblank event string. It does not accept an array, newline-delimited JSON, or several concatenated objects. Those can be valid protocols when explicitly designed, but they are different contracts.
Decoder.Decode reads the next JSON value from a stream. One call therefore does not certify that the stream has ended. In contrast, the fixture also shows Unmarshal rejecting a byte slice that contains two concatenated top-level objects.
2. Run the boundary checks
Save this as event-body.go and run go run event-body.go. It needs only the standard library and makes no network requests. The event names are synthetic; there is no HTTP server or storage write.
package main
import (
"encoding/json"
"errors"
"fmt"
"io"
"strings"
)
type Event struct {
Event string `json:"event"`
}
func oneEvent(raw string) (Event, error) {
dec := json.NewDecoder(strings.NewReader(raw))
dec.DisallowUnknownFields()
var event Event
if err := dec.Decode(&event); err != nil {
return Event{}, fmt.Errorf("first value: %w", err)
}
var extra any
if err := dec.Decode(&extra); !errors.Is(err, io.EOF) {
if err == nil {
return Event{}, errors.New("second JSON value")
}
return Event{}, fmt.Errorf("trailing input: %w", err)
}
if strings.TrimSpace(event.Event) == "" {
return Event{}, errors.New("event must be nonblank")
}
return event, nil
}
func require(ok bool, message string) {
if !ok {
panic(message)
}
}
func main() {
cases := []struct {
name, input string
wantOK bool
}{
{"one object", `{"event":"PageViewed"}`, true},
{"trailing whitespace", "{\"event\":\"PageViewed\"}\n\t ", true},
{"second object", `{"event":"PageViewed"}{"event":"Paid"}`, false},
{"second null", `{"event":"PageViewed"} null`, false},
{"trailing garbage", `{"event":"PageViewed"} broken`, false},
{"array", `[{"event":"PageViewed"}]`, false},
{"empty body", "", false},
{"null event", `null`, false},
{"missing name", `{}`, false},
{"blank name", `{"event":" "}`, false},
{"unknown field", `{"event":"PageViewed","extra":1}`, false},
}
for _, tc := range cases {
_, err := oneEvent(tc.input)
require((err == nil) == tc.wantOK, tc.name)
}
two := `{"event":"PageViewed"}{"event":"Paid"}`
var first Event
err := json.NewDecoder(strings.NewReader(two)).Decode(&first)
require(err == nil && first.Event == "PageViewed", "one Decode reads first")
err = json.Unmarshal([]byte(two), &Event{})
require(err != nil, "Unmarshal rejects two values")
duplicate, err := oneEvent(`{"event":"PageViewed","event":"Paid"}`)
require(err == nil && duplicate.Event == "Paid", "duplicate field remains")
fmt.Printf("PASS: %d contract cases and 3 behavior checks\n", len(cases))
}
The second decode has three relevant outcomes: EOF permits trailing whitespace; another successfully decoded value violates this contract; any other error also fails. In particular, a second null is still another value, even though it contributes no event fields.
3. Keep framing and field rules separate
DisallowUnknownFields is included because this fixture deliberately uses a closed struct contract. It does not mean every evolving event schema should reject newly introduced properties during a rolling release.
The final behavior check is intentional: the v1 decoder accepts a duplicate event member and this struct ends up with Paid. The EOF check does not reject duplicate names inside one object. V1 struct matching also permits case-insensitive field matches; this is not an exact-spelling schema validator.
Likewise, decoding null into a fresh struct does not itself establish a valid event. The nonblank-name check rejects the resulting zero value here. A real event contract needs its own names, property types, required fields and version policy; do not interpret this small fixture as a complete validator.
4. Validate the same stream, then address resource limits
Use the same decoder for the second check. NewDecoder can buffer bytes beyond the value returned, so checking only the underlying reader can overlook bytes already buffered by the decoder.
This in-memory example has finite strings. An HTTP implementation also needs an explicit request-size limit and appropriate server timeouts; an EOF check can otherwise wait for more input. MaxBytesReader is the standard HTTP helper for limiting incoming body reads. Compressed input needs its own decompressed-size policy as well; the fixture does not implement compression or network handling.
Before acknowledging an event, record what was validated: transport framing, exactly one accepted JSON value, a compatible event contract, and the stage actually completed. Passing the parser does not prove that the event was stored, deduplicated, or included in a report.
來源:DEV Community · dev.to