{"record":{"id":"c9b4131c78023dcc","repo":"hashicorp/packer","slug":"malformed-index-json-w","errorCode":null,"errorMessage":"malformed index.json: %w","messagePattern":"malformed index\\.json: %w","errorType":"exception","errorClass":null,"httpStatus":null,"severity":"error","filePath":"packer/plugin-getter/remote/getter.go","lineNumber":79,"sourceCode":"// entryHasProtocolVersion reports whether a checksum-file entry name carries\n// an x-prefixed protocol version field.\nvar entryHasProtocolVersion = regexp.MustCompile(`_x\\d+\\.\\d+_`)\n\n// pluginIndex is the validated form of a plugin's index.json. Versions are\n// keyed by their canonical core string (e.g. \"1.1.4\"); all other index\n// content is ignored.\ntype pluginIndex struct {\n\tpluginType string\n\tversions   map[string]struct{}\n}\n\n// parseIndex validates raw index.json content into a pluginIndex.\nfunc parseIndex(data []byte, pluginType string) (*pluginIndex, error) {\n\tvar raw struct {\n\t\tVersions map[string]struct{} `json:\"versions\"`\n\t}\n\tif err := json.Unmarshal(data, &raw); err != nil {\n\t\treturn nil, fmt.Errorf(\"malformed index.json: %w\", err)\n\t}\n\tif len(raw.Versions) == 0 {\n\t\treturn nil, fmt.Errorf(\"index.json contains no versions\")\n\t}\n\n\tidx := &pluginIndex{\n\t\tpluginType: pluginType,\n\t\tversions:   map[string]struct{}{},\n\t}\n\tfor k := range raw.Versions {\n\t\tver, err := goversion.NewVersion(k)\n\t\tif err != nil {\n\t\t\tlog.Printf(\"[WARN] remote-getter: ignoring unparseable version %q in index.json\", k)\n\t\t\tcontinue\n\t\t}\n\t\tidx.versions[ver.String()] = struct{}{}\n\t}\n\tif len(idx.versions) == 0 {","sourceCodeStart":61,"sourceCodeEnd":97,"githubUrl":"https://github.com/hashicorp/packer/blob/eb36e3c3e48a036f3e8cc94087636ee72e1303c9/packer/plugin-getter/remote/getter.go#L61-L97","documentation":"The remote Getter caches plugin versions in an index.json whose expected shape is {\"versions\": {...}}. parseIndex unmarshals the fetched bytes into that structure; if the body is not valid JSON (or `versions` isn't an object), it wraps the unmarshal error as \"malformed index.json\". Called by loadIndex whenever the cached index is missing or stale.","triggerScenarios":"loadIndex fetches index.json and parseIndex receives bytes that fail json.Unmarshal — e.g. an HTML error page, an empty body, or truncated download served at the index URL.","commonSituations":"A corporate proxy or captive portal returning an HTML login page instead of JSON; a partially written/corrupted cached index.json on disk; a custom PACKER_PLUGIN_CONFIG_PATH layout with a hand-edited index.json; CDN error responses with status < 400.","solutions":["Inspect the URL/body returned for the index (curl it) to see what non-JSON content is being served.","Delete the corrupted cached index.json in the plugin directory and re-run the install so it is re-fetched.","Fix proxy/network equipment that is injecting HTML instead of passing through the JSON response."],"exampleFix":"// before (index.json on disk)\n<html>Moved Temporarily</html>\n// after\n{\n  \"versions\": {\n    \"1.1.1\": {}\n  }\n}","handlingStrategy":"try-catch","validationCode":"body, err := os.ReadFile(indexPath)\nif err == nil && !json.Valid(body) {\n    os.Remove(indexPath) // corrupt cache; force re-fetch\n}","typeGuard":null,"tryCatchPattern":"_, err := g.Get(\"releases\", opts)\nif err != nil && strings.Contains(err.Error(), \"malformed index.json\") {\n    os.Remove(cachedIndexJSON)\n    _, err = g.Get(\"releases\", opts) // re-fetch clean index\n}","preventionTips":["Never hand-edit index.json in the plugin directory.","Ensure proxies don't inject HTML into JSON responses.","Validate JSON after interrupted downloads before trusting a cache."],"tags":["packer","plugin-getter","json-parsing","remote-fetch"],"backgroundTag":"invalid-json-response","analyzedSha":"eb36e3c3e48a036f3e8cc94087636ee72e1303c9","analyzedAt":"2026-09-05T13:20:43.127Z","contentChangedAt":"2026-09-05T13:20:43.127Z","schemaVersion":2},"datasetVersion":"2026-09-12T17:17:11.597Z"}