MHSanaei/3x-ui · error

write panel updater: %w

Error message

write panel updater: %w

What it means

Returned when io.Copy fails while streaming the panel updater binary from the GitHub release response body into a temp file (os.CreateTemp). The %w wraps the underlying filesystem error, so the real cause (disk full, temp dir permission, I/O interruption) is in the wrapped error chain. The deferred cleanup already removes the partial temp file, so no residue is left behind.

Source

Thrown at internal/web/service/panel/panel.go:387

		return "", fmt.Errorf("download panel updater: unexpected HTTP %d", resp.StatusCode)
	}

	file, err := os.CreateTemp("", "3x-ui-update-*.sh")
	if err != nil {
		return "", err
	}
	path := file.Name()
	ok := false
	defer func() {
		_ = file.Close()
		if !ok {
			_ = os.Remove(path)
		}
	}()

	n, err := io.Copy(file, io.LimitReader(resp.Body, maxPanelUpdaterBytes+1))
	if err != nil {
		return "", fmt.Errorf("write panel updater: %w", err)
	}
	if n == 0 {
		return "", fmt.Errorf("panel updater download is empty")
	}
	if n > maxPanelUpdaterBytes {
		return "", fmt.Errorf("panel updater exceeds %d bytes", maxPanelUpdaterBytes)
	}
	if err := file.Chmod(0o700); err != nil {
		return "", err
	}
	ok = true
	return path, nil
}

func fetchLatestPanelVersion() (string, error) {
	release, err := fetchPanelRelease("")
	if err != nil {
		return "", err

View on GitHub (pinned to ad32144c42)

Solutions

  1. Check the wrapped error: run df -h /tmp and df -h / to confirm disk space, and verify TMPDIR is writable (touch $TMPDIR/x)
  2. If a proxy is configured for outbound updates, verify it does not truncate large downloads
  3. Retry the update after freeing space or pointing TMPDIR at a writable, executable filesystem

Example fix

// before: opaque failure when /tmp is full
// (fix at the ops level)
// after: point TMPDIR at a writable disk before updating
// export TMPDIR=/var/tmp && x-ui update-panel
Defensive patterns

Strategy: try-catch

Validate before calling

// Before triggering the update, check the temp filesystem
if f, err := os.CreateTemp("", "probe"); err != nil {
    log.Printf("temp dir unusable: %v", err)
} else {
    f.Close()
    os.Remove(f.Name())
}

Try / catch

path, err := panel.DownloadPanelUpdater()
if err != nil {
    var pathErr *os.PathError
    if errors.As(err, &pathErr) && errors.Is(pathErr.Err, syscall.ENOSPC) {
        // surface 'disk full' to the operator, not a raw stack
    }
    return fmt.Errorf("panel update download failed: %w", err)
}

Prevention

When it happens

Trigger: downloadPanelUpdater() runs after a successful HTTP GET of the updater asset; io.Copy aborts because the temp filesystem is full (ENOSPC), TMPDIR points to an unwritable/read-only directory, or the response body reader errors mid-stream (connection reset through a proxy).

Common situations: Small VPS with a full /tmp or root partition; hardened systems where /tmp is mounted noexec/read-only; flaky egress or an HTTP proxy that truncates long responses.

Related errors


AI-assisted analysis of MHSanaei/3x-ui@ad32144c42 (2026-08-15). Data as JSON: /api/errors/1354b5a42edca778. Report an issue: GitHub.