XML Decode Error Annotation#
Problem: XML field type mismatches (e.g., a field typed as int receiving the value "on") produce bare Go runtime errors like strconv.ParseInt: parsing "on": invalid syntax with no indication of which XML element is at fault. Issue #558 is a concrete example: a user got this error with no element path, making the offending field unlocatable.
Solution: The project annotates XML decode errors with element path context using two helpers in internal/cfgparser/errors.go:
WrapXMLSyntaxError(err, elementPath)— wraps anxml.SyntaxErrorinto acfgparser.ParseErrorwith element path embedded in the message, e.g.,"unexpected EOF (in element path: opnsense.system)".WrapXMLSyntaxErrorWithOffset(err, elementPath, dec)— enhanced version that also appends the current byte offset fromdec.InputOffset()for even more precise location, e.g.,"(at byte offset: 4096)".
Both produce a cfgparser.ParseError, which carries Line, Column, and Message and is detectable via cfgparser.IsParseError(err).
Note: The background context references
parser.WrapDecodeError()— this function does not exist in the current codebase. The canonical helpers areWrapXMLSyntaxErrorandWrapXMLSyntaxErrorWithOffsetininternal/cfgparser/errors.go.
OPNsense Wiring (Section-by-Section)#
The OPNsense XML parser in internal/cfgparser/xml.go uses a streaming, token-based approach. Each top-level XML element under <opnsense> is individually decoded by handleStartElement(), which dispatches to a section-specific decodeChild() call (30+ cases: system, interfaces, nat, filter, gateways, openvpn, etc.).
When the token loop encounters a decode error, it calls handleXMLError():
func handleXMLError(err error, dec *xml.Decoder) error {
if wrappedErr := WrapXMLSyntaxErrorWithOffset(err, "opnsense", dec); wrappedErr != nil {
return fmt.Errorf("failed to decode XML: %w", wrappedErr)
}
return fmt.Errorf("failed to read token: %w", err)
}
This means XML syntax errors from OPNsense parsing will carry the element path "opnsense" in their message. The path is coarse (document root only) — individual section names are not appended at the error annotation level, though the error message from within a section decode will often identify the offending token in context.
The decodeChild() helper is a thin wrapper around dec.DecodeElement() and does not add additional path context itself. Errors from DecodeElement (such as field-type mismatches that produce strconv errors) bubble up through handleStartElement and return directly to Parse() — they do not pass through handleXMLError, so they are not wrapped as cfgparser.ParseError and carry no element path. This is a known gap: decode errors from within a section are currently indistinguishable from general errors at the exit-code and JSON output level.
pfSense Wiring (Document-Root, Single-Pass)#
The pfSense parser in pkg/parser/pfsense/parser.go decodes the entire document in a single dec.Decode(&doc) call inside decode():
if err := dec.Decode(&doc); err != nil {
return nil, fmt.Errorf("XML decode: %w", err)
}
This wraps the raw encoding/xml error with the prefix "XML decode:" but does not call WrapXMLSyntaxError or WrapXMLSyntaxErrorWithOffset. Consequently, pfSense decode errors are not typed as cfgparser.ParseError — they are plain fmt.Errorf-wrapped errors.
What this means in practice:
- Error messages lack element path context beyond
"pfsense parser: XML decode: ...". cfgparser.IsParseError(err)returnsfalsefor these errors.DetermineExitCode()falls through toExitGeneralError (1)rather thanExitParseError (2).
Deeper per-section path annotation for pfSense (analogous to the OPNsense section switch) is tracked as a follow-up item (referenced in the background context as todo 099, though that file is not present in the current repo HEAD).
Structured Error Propagation Gaps (Exit Codes & JSONError)#
Exit Codes#
cmd/exitcodes.go defines the exit code ladder:
| Code | Constant | Meaning |
|---|---|---|
| 0 | ExitSuccess | OK |
| 1 | ExitGeneralError | Unknown/general error |
| 2 | ExitParseError | XML parse failure |
| 3 | ExitValidationError | Schema validation failure |
| 4 | ExitFileError | File I/O error |
DetermineExitCode() uses cfgparser.IsParseError(err) to gate ExitParseError. Because pfSense decode errors are not wrapped in cfgparser.ParseError, they fall through and emit ExitGeneralError (1) — masking parse failures as generic errors in CI/CD pipelines.
JSONError.Details#
OutputJSONError() populates JSONError.Details only when cfgparser.IsParseError(err) is true, and only with line and message fields:
jsonErr.Details = map[string]any{
"line": parseErr.Line,
"message": parseErr.Message,
}
There is currently no element_path field in JSONError.Details. Automation consumers that need to locate the offending XML element must regex-scrape the message string for the "(in element path: ...)" fragment — a fragile approach. Adding a dedicated element_path key is future work.
Requirements for New Device Parsers#
Any new device parser registered with parser.Register() should follow these rules at its decode boundary:
- Use
WrapXMLSyntaxErrorWithOffset(err, "<root-element>", dec)to convert rawencoding/xmlerrors intocfgparser.ParseError. This ensurescfgparser.IsParseError()returnstrue,DetermineExitCode()maps toExitParseError (2), andOutputJSONError()populatesJSONError.Details. - Pass the most specific available element path as the second argument — at minimum the document root element name (e.g.,
"pfsense","opnsense"). For section-by-section parsers, pass the section name (e.g.,"opnsense.interfaces"). - Do not suppress decode errors with a workaround
catch-all. Anystrconv.ParseInt,strconv.ParseBool, orstrconv.Atoierror appearing in the stack trace is almost certainly a schema miscategorisation — the struct field is typed wrong relative to the XML values the device emits. Fix the schema type; do not add a custom error handler to swallow it.
Schema Bug Rule#
If a bug report shows
strconv.ParseInt,strconv.ParseBool, orstrconv.Atoiin the error text, treat it as a field typed incorrectly in the schema (e.g.,intwhere the device uses"on"/"yes"). See issue #558 for a canonical example.
Reference files:
- Error helpers:
internal/cfgparser/errors.go - OPNsense wiring:
internal/cfgparser/xml.go - pfSense wiring:
pkg/parser/pfsense/parser.go - Exit codes & JSON output:
cmd/exitcodes.go - Secure XML decoder:
pkg/parser/xmlutil.go