hashicorp/packer · error

malformed index.json: %w

Error message

malformed index.json: %w

What it means

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.

Source

Thrown at packer/plugin-getter/remote/getter.go:79

// entryHasProtocolVersion reports whether a checksum-file entry name carries
// an x-prefixed protocol version field.
var entryHasProtocolVersion = regexp.MustCompile(`_x\d+\.\d+_`)

// pluginIndex is the validated form of a plugin's index.json. Versions are
// keyed by their canonical core string (e.g. "1.1.4"); all other index
// content is ignored.
type pluginIndex struct {
	pluginType string
	versions   map[string]struct{}
}

// parseIndex validates raw index.json content into a pluginIndex.
func parseIndex(data []byte, pluginType string) (*pluginIndex, error) {
	var raw struct {
		Versions map[string]struct{} `json:"versions"`
	}
	if err := json.Unmarshal(data, &raw); err != nil {
		return nil, fmt.Errorf("malformed index.json: %w", err)
	}
	if len(raw.Versions) == 0 {
		return nil, fmt.Errorf("index.json contains no versions")
	}

	idx := &pluginIndex{
		pluginType: pluginType,
		versions:   map[string]struct{}{},
	}
	for k := range raw.Versions {
		ver, err := goversion.NewVersion(k)
		if err != nil {
			log.Printf("[WARN] remote-getter: ignoring unparseable version %q in index.json", k)
			continue
		}
		idx.versions[ver.String()] = struct{}{}
	}
	if len(idx.versions) == 0 {

View on GitHub (pinned to eb36e3c3e4)

Solutions

  1. Inspect the URL/body returned for the index (curl it) to see what non-JSON content is being served.
  2. Delete the corrupted cached index.json in the plugin directory and re-run the install so it is re-fetched.
  3. Fix proxy/network equipment that is injecting HTML instead of passing through the JSON response.

Example fix

// before (index.json on disk)
<html>Moved Temporarily</html>
// after
{
  "versions": {
    "1.1.1": {}
  }
}
Defensive patterns

Strategy: try-catch

Validate before calling

body, err := os.ReadFile(indexPath)
if err == nil && !json.Valid(body) {
    os.Remove(indexPath) // corrupt cache; force re-fetch
}

Try / catch

_, err := g.Get("releases", opts)
if err != nil && strings.Contains(err.Error(), "malformed index.json") {
    os.Remove(cachedIndexJSON)
    _, err = g.Get("releases", opts) // re-fetch clean index
}

Prevention

When it happens

Trigger: 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.

Common situations: 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.

Understand the failure class

Background: "Invalid JSON response" and "Failed to parse response" errors: when an API answers 200 but the body isn't the JSON your library expected — this error's family across 28 libraries.

Related errors


AI-assisted analysis of hashicorp/packer@eb36e3c3e4 (2026-09-05). Data as JSON: /api/errors/c9b4131c78023dcc. Report an issue: GitHub.