k3s-io/k3s · error

only wrote %d bytes to %s; expected %d

Error message

only wrote %d bytes to %s; expected %d

What it means

io.Copy into the extracted file returned no error but wrote fewer bytes than the tar header declared (f.Size). The tar stream is therefore shorter than its header claims - usually a truncated download, a zstd stream cut mid-entry, or a corrupt archive - and the extractor refuses to leave a silently incomplete file on disk.

Source

Thrown at pkg/untar/untar.go:91

			if !madeDir[dir] {
				if err := os.MkdirAll(filepath.Dir(abs), 0755); err != nil {
					return err
				}
				madeDir[dir] = true
			}
			wf, err := os.OpenFile(abs, os.O_RDWR|os.O_CREATE|os.O_TRUNC, mode.Perm())
			if err != nil {
				return err
			}
			n, err := io.Copy(wf, tr)
			if closeErr := wf.Close(); closeErr != nil && err == nil {
				err = closeErr
			}
			if err != nil {
				return fmt.Errorf("error writing to %s: %v", abs, err)
			}
			if n != f.Size {
				return fmt.Errorf("only wrote %d bytes to %s; expected %d", n, abs, f.Size)
			}
			modTime := f.ModTime
			if modTime.After(t0) {
				// Clamp modtimes at system time. See
				// golang.org/issue/19062 when clock on
				// buildlet was behind the gitmirror server
				// doing the git-archive.
				modTime = t0
			}
			if !modTime.IsZero() {
				if err := os.Chtimes(abs, modTime, modTime); err != nil && !loggedChtimesError {
					// benign error. Gerrit doesn't even set the
					// modtime in these, and we don't end up relying
					// on it anywhere (the gomote push command relies
					// on digests only), so this is a little pointless
					// for now.
					logrus.Printf("error changing modtime: %v (further Chtimes errors suppressed)", err)
					loggedChtimesError = true // once is enough

View on GitHub (pinned to 6ba341e396)

Solutions

  1. Verify integrity at the source: compare sha256sum with the producer, or run zstd -t file.tar.zst
  2. Re-download or regenerate the tarball and retry the extraction
  3. If you build the archive yourself, ensure tar.Close() and the zstd writer are flushed/closed before shipping
  4. Check producer-side disk space if the archive was created on a nearly full host
Defensive patterns

Strategy: validation

Validate before calling

func VerifyBundle(path, wantSHA256 string) error {
	f, err := os.Open(path)
	if err != nil {
		return err
	}
	defer f.Close()
	h := sha256.New()
	if _, err := io.Copy(h, f); err != nil {
		return err
	}
	if got := hex.EncodeToString(h.Sum(nil)); got != wantSHA256 {
		return fmt.Errorf("checksum mismatch: got %s want %s", got, wantSHA256)
	}
	return nil
}

Try / catch

if err := untar.Untar(r, dir); err != nil {
	if strings.Contains(err.Error(), "only wrote") && strings.Contains(err.Error(), "expected") {
		// truncated/corrupt archive: delete the partial dir and re-download
		os.RemoveAll(dir)
	}
	return err
}

Prevention

When it happens

Trigger: untar.Untar on a tarball truncated mid-file-entry (interrupted download, proxy cut-off, partial copy); a zstd stream that decodes short; a header whose Size is larger than the data actually stored.

Common situations: Image/airgap bundles partially downloaded over flaky networks; artifacts truncated by CI artifact size caps or storage limits; producer wrote the archive on a full disk or did not close/flush the tar and zstd writers before shipping.

Related errors


AI-assisted analysis of k3s-io/k3s@6ba341e396 (2026-08-15). Data as JSON: /api/errors/312ad46b22d26b20. Report an issue: GitHub.