{"record":{"id":"1dde49104ffdbf56","repo":"Tencent/WeKnora","slug":"decode-sandbox-binding-w","errorCode":null,"errorMessage":"decode sandbox binding: %w","messagePattern":"decode sandbox binding: %w","errorType":"exception","errorClass":null,"httpStatus":null,"severity":"error","filePath":"internal/sandbox/session_binding_redis.go","lineNumber":127,"sourceCode":"// Get returns the current binding, or nil when the session is unbound.\nfunc (s *RedisSessionSandboxBindingStore) Get(\n\tctx context.Context,\n\tkey SessionSandboxKey,\n) (*SessionSandboxBinding, error) {\n\tif err := key.Validate(); err != nil {\n\t\treturn nil, err\n\t}\n\traw, err := s.client.Get(ctx, s.bindingKey(key)).Bytes()\n\tif errors.Is(err, redis.Nil) {\n\t\treturn nil, nil\n\t}\n\tif err != nil {\n\t\treturn nil, fmt.Errorf(\"get sandbox binding: %w\", err)\n\t}\n\n\tvar binding SessionSandboxBinding\n\tif err := json.Unmarshal(raw, &binding); err != nil {\n\t\treturn nil, fmt.Errorf(\"decode sandbox binding: %w\", err)\n\t}\n\tif err := binding.Validate(key); err != nil {\n\t\treturn nil, fmt.Errorf(\"validate sandbox binding: %w\", err)\n\t}\n\treturn &binding, nil\n}\n\n// Create stores a validated current-schema binding with SET NX and no\n// expiration.\nfunc (s *RedisSessionSandboxBindingStore) Create(\n\tctx context.Context,\n\tkey SessionSandboxKey,\n\tbinding SessionSandboxBinding,\n) (bool, error) {\n\tif err := binding.Validate(key); err != nil {\n\t\treturn false, err\n\t}\n\traw, err := json.Marshal(binding)","sourceCodeStart":109,"sourceCodeEnd":145,"githubUrl":"https://github.com/Tencent/WeKnora/blob/988cbb03305e055d8ebb7d46d9ac6cc0803cd074/internal/sandbox/session_binding_redis.go#L109-L145","documentation":"This error is returned by RedisSessionSandboxBindingStore.Get when the JSON value stored under the session's binding key in Redis cannot be unmarshaled into SessionSandboxBinding. The binding key holds JSON with no TTL, so the store throws this when the stored value is corrupt, truncated, written by a different schema version, or not JSON at all (e.g. hand-edited or written by another tool). It wraps the underlying json.Unmarshal error with 'decode sandbox binding: %w'.","triggerScenarios":"Calling Get(ctx, key) when the Redis string at weknora:sandbox:session:{<ns>:<tenant>:<session>}:binding is corrupt/truncated JSON, was written by an older or newer schema version whose fields no longer unmarshal, was manually overwritten with non-JSON data, or contains types incompatible with the SessionSandboxBinding struct (e.g. StaleAt not a timestamp).","commonSituations":"Version-skew during a rolling upgrade where an older build wrote a legacy binding format; an operator or cleanup script SET the key directly with wrong content; Redis persistence restored a truncated value after a crash; manual inspection tools (redis-cli) accidentally modified the key.","solutions":["Run redis-cli GET 'weknora:sandbox:session:{<namespace>:<tenantID>:<sessionID>}:binding' and inspect whether the value is valid JSON matching the current SessionSandboxBinding schema","If the value is corrupt or legacy, delete it with DEL — Get then returns (nil, nil) as an unbound session and the next resolve recreates the binding","Roll out the current build fully so no old process writes legacy-format bindings mid-flight","If corruption is recurring, check for external writers/scripts touching the weknora:sandbox:* key space and check Redis persistence health (AOF/RDB integrity)"],"exampleFix":"// before: error surfaces to caller as opaque decode failure\nbinding, err := store.Get(ctx, key)\nif err != nil { return err }\n// after: recover by treating an undecodable binding as unbound and recreating it\nbinding, err := store.Get(ctx, key)\nif err != nil {\n    if strings.Contains(err.Error(), \"decode sandbox binding\") {\n        _ = store.DeleteIfMatch(ctx, key, provider, sandboxID)\n        created, cerr := store.Create(ctx, key, SessionSandboxBinding{Provider: provider, SandboxID: sandboxID})\n        if cerr != nil { return cerr }\n        _ = created\n        binding, err = store.Get(ctx, key)\n    }\n    if err != nil { return err }\n}","handlingStrategy":"try-catch","validationCode":"// best-effort pre-check that the stored value is parseable JSON before relying on Get\nfunc bindingLooksValid(ctx context.Context, rdb *redis.Client, key string) bool {\n    raw, err := rdb.Get(ctx, \"weknora:sandbox:session:{\"+key+\"}:binding\").Bytes()\n    return err == nil && json.Valid(raw)\n}","typeGuard":"func isDecodeBindingError(err error) bool {\n    return err != nil && strings.Contains(err.Error(), \"decode sandbox binding\")\n}","tryCatchPattern":"binding, err := store.Get(ctx, key)\nif isDecodeBindingError(err) {\n    // corrupt record: treat session as unbound and let resolve recreate the binding\n    log.Printf(\"corrupt binding for %s: %v\", key.SessionID, err)\n    binding, err = nil, nil\n} else if err != nil {\n    return fmt.Errorf(\"get binding: %w\", err)\n}","preventionTips":["Never SET weknora:sandbox:* keys manually or from scripts outside the store API","Keep SessionSandboxBinding schema changes backward-compatible during rolling upgrades","Add a startup smoke test that Get/Create/Get a throwaway session key","Monitor decode-failure logs — a spike indicates external writers or persistence corruption"],"tags":["redis","json-decode","data-corruption","go"],"backgroundTag":"redis-binding-json-decode-failed","analyzedSha":"988cbb03305e055d8ebb7d46d9ac6cc0803cd074","analyzedAt":"2026-09-02T14:41:08.344Z","contentChangedAt":null,"schemaVersion":2},"datasetVersion":"2026-09-08T15:18:49.778Z"}