siyuan-note/siyuan · error
struct field Sync .
Error message
struct field Sync%s.
What it means
This rewritten error is produced by syncConfigError when decoding POST /api/sync/setSyncProviderS3, /setSyncProviderWebDAV, or /setSyncProviderLocal. The raw reflection error from unmarshalling the nested provider config mentions internal names like `struct field SyncS3.Endpoint`; the rewriter normalizes them to user-facing config names (`struct field S3.Endpoint`). The underlying problem is one field inside the provider object has the wrong JSON type.
Solutions
- Check the exact field named in the message and send the correct JSON type (numbers as numbers, booleans as booleans)
- Compare the payload against the documented SyncS3/SyncWebDAV/SyncLocal object shape
- Read the request.ConfigError() on the returned object — for these endpoints the error is attached to the response, not a hard failure
Example fix
// before
{"s3": {"endpoint": "https://s3.example.com", "timeout": "30"}}
// after
{"s3": {"endpoint": "https://s3.example.com", "timeout": 30}} Defensive patterns
Strategy: validation
Validate before calling
const num = (v) => typeof v === 'number' && !isNaN(v); const validS3 = s => s && typeof s === 'object' && (s.timeout === undefined || num(s.timeout)) && (s.concurrentReqs === undefined || num(s.concurrentReqs));
Type guard
const isProviderConfig = (v) => typeof v === 'object' && v !== null && !Array.isArray(v);
Try / catch
const req = await fetchPost('/api/sync/setSyncProviderS3', {s3: cfg}); if (req.code !== 0 || (req.data && req.data.configError)) console.error(req.msg); Prevention
- Keep numbers as JSON numbers, booleans as JSON booleans
- Compare payload to the documented SyncS3/SyncWebDAV/SyncLocal schema
- Avoid stringifying config values in settings UIs before sending
When it happens
Trigger: POSTing /api/sync/setSyncProviderS3 with e.g. {"s3":{"timeout":"30"}} (string where number expected) or {"s3":{"skipTlsVerify":"yes"}}; same for webdav/local nested objects.
Common situations: Config copied from a YAML/INI export where numbers became strings; a settings UI storing values as strings and forwarding them unconverted; API version drift between client payload shape and kernel struct.
Related errors
- invalid config.
- map[string]apicontract.JSONValue
- block ID must be text
- block operation result must be text, block IDs or null
- builtin color must not be null
AI-assisted analysis of siyuan-note/siyuan@9f775e8a12 (2026-09-19).
Data as JSON: /api/errors/0e2a9b1289ec31cc.
Report an issue: GitHub.
Appendix: source
Thrown at kernel/apicontract/sync.go:139
Updated string `json:"updated"`
CloudName string `json:"cloudName"`
SaveDir string `json:"saveDir"`
}
type CloudSyncDirsData struct {
SyncDirs []*CloudSyncDir `json:"syncDirs"`
HSize string `json:"hSize"`
CheckedSyncDir string `json:"checkedSyncDir"`
}
type SyncAssetDownloadModeData struct {
AssetDownloadMode int `json:"assetDownloadMode"`
}
func syncConfigError(err error, name string) error {
if err == nil {
return nil
}
detail := strings.ReplaceAll(err.Error(), "apicontract.Sync"+name, "conf."+name)
return errors.New(strings.ReplaceAll(detail, "struct field Sync"+name+".", "struct field "+name+"."))
}
// syncRequestFields 保留整份请求的 JSON 数字解析、重复字段和首个对象读取语义。
func syncRequestFields(reader io.Reader, path string) (fields map[string]json.RawMessage, err error) {
var raw json.RawMessage
err = json.NewDecoder(reader).Decode(&raw)
if err == nil {
fields, err = legacyJSONValue[map[string]json.RawMessage](raw)
}
if err != nil {
if errors.Is(err, io.EOF) {
err = errors.New("the request body is empty or truncated (EOF)")
}
detail := strings.ReplaceAll(err.Error(), "map[string]json.RawMessage", "map[string]interface {}")
err = fmt.Errorf("Parses request [%s] failed: %s", path, detail)
}
return
}View on GitHub (pinned to 9f775e8a12)