{"record":{"id":"6ce2a7a62d4deb5c","repo":"bytebase/bytebase","slug":"failed-to-marshal-masked-responses-column","errorCode":null,"errorMessage":"failed to marshal masked responses column","messagePattern":"failed to marshal masked responses column","errorType":"error_code","errorClass":null,"httpStatus":null,"severity":"error","filePath":"backend/api/v1/document_masking.go","lineNumber":1288,"sourceCode":"func maskElasticsearchMSearchResponses(responsesColumnJSON string, sortFields []string, objectSchema *storepb.ObjectSchema, semanticTypeToMasker map[string]masker.Masker) (string, error) {\n\tvar responses []any\n\tif err := json.Unmarshal([]byte(responsesColumnJSON), &responses); err != nil {\n\t\treturn \"\", errors.Wrap(err, \"failed to unmarshal responses column\")\n\t}\n\n\tfor i, resp := range responses {\n\t\trespMap, ok := resp.(map[string]any)\n\t\tif !ok {\n\t\t\tcontinue\n\t\t}\n\t\tif err := maskElasticsearchMSearchSingleResponse(respMap, sortFields, objectSchema, semanticTypeToMasker); err != nil {\n\t\t\treturn \"\", errors.Wrapf(err, \"failed to mask response %d\", i)\n\t\t}\n\t}\n\n\tout, err := json.Marshal(responses)\n\tif err != nil {\n\t\treturn \"\", errors.Wrap(err, \"failed to marshal masked responses column\")\n\t}\n\treturn string(out), nil\n}\n\n// maskElasticsearchMSearchSingleResponse masks the hits in a single _msearch response element.\nfunc maskElasticsearchMSearchSingleResponse(respMap map[string]any, sortFields []string, objectSchema *storepb.ObjectSchema, semanticTypeToMasker map[string]masker.Masker) error {\n\thitsVal, ok := respMap[\"hits\"]\n\tif !ok {\n\t\treturn nil\n\t}\n\thitsObj, ok := hitsVal.(map[string]any)\n\tif !ok {\n\t\treturn nil\n\t}\n\thitsArray, ok := hitsObj[\"hits\"].([]any)\n\tif !ok {\n\t\treturn nil\n\t}","sourceCodeStart":1270,"sourceCodeEnd":1306,"githubUrl":"https://github.com/bytebase/bytebase/blob/1870550677fe08f0d2a78c07acd27541464eb945/backend/api/v1/document_masking.go#L1270-L1306","documentation":"After all _msearch response elements are masked, the responses slice is re-serialized with json.Marshal back into the stored column string. Failure here means one of the (possibly masker-modified) values is no longer JSON-serializable, aborting the masking step.","triggerScenarios":"json.Marshal(responses) fails after maskElasticsearchMSearchSingleResponse has run — typically a masker replaced a value with a channel/func/invalid float, or produced a cyclic or unsupported structure.","commonSituations":"Custom maskers returning non-JSON Go types; NaN/Inf float results from numeric transformations; internal mutation of response maps with unsupported values.","solutions":["Ensure every masker emits only JSON-basic values (nil, bool, string, float64, json.Number, []any, map[string]any)","Inspect the wrapped inner error to locate the offending value","Sanitize masker output (convert unknown types to strings or null) before writing back into respMap","Add a unit test with the actual payload that reproduces the marshal failure"],"exampleFix":null,"handlingStrategy":"try-catch","validationCode":"if _, err := json.Marshal(responses); err != nil { /* sanitize before final marshal */ }","typeGuard":"func jsonSafe(v any) bool { return isJSONBasicDeep(v) }","tryCatchPattern":"out, err := json.Marshal(responses)\nif err != nil {\n    sanitizeNonSerializable(responses)\n    out, err = json.Marshal(responses)\n}","preventionTips":["Constrain masker return types to JSON-basic values","Fuzz-test maskers with unusual document values","Sanitize response maps after masking, before marshal"],"tags":["go","json","elasticsearch","data-masking"],"backgroundTag":"json-marshal-failed","analyzedSha":"1870550677fe08f0d2a78c07acd27541464eb945","analyzedAt":"2026-09-06T21:16:13.665Z","contentChangedAt":"2026-09-06T21:16:13.665Z","schemaVersion":2},"datasetVersion":"2026-09-14T05:17:10.506Z"}