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 "", errView on GitHub (pinned to ad32144c42)
Solutions
- Check the wrapped error: run df -h /tmp and df -h / to confirm disk space, and verify TMPDIR is writable (touch $TMPDIR/x)
- If a proxy is configured for outbound updates, verify it does not truncate large downloads
- 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
- Monitor free space on the filesystem holding TMPDIR before scheduling updates
- Run update operations behind a retry wrapper with jittered backoff
- Do not point TMPDIR at a read-only or noexec mount on update-running hosts
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
- panel updater download is empty
- panel updater exceeds %d bytes
- sqlite backup destination already exists: %s
- source sqlite not found at %s: %w
- destination already exists: %s
AI-assisted analysis of MHSanaei/3x-ui@ad32144c42 (2026-08-15).
Data as JSON: /api/errors/1354b5a42edca778.
Report an issue: GitHub.