{"record":{"id":"1354b5a42edca778","repo":"MHSanaei/3x-ui","slug":"write-panel-updater-w","errorCode":null,"errorMessage":"write panel updater: %w","messagePattern":"write panel updater: %w","errorType":"exception","errorClass":null,"httpStatus":null,"severity":"error","filePath":"internal/web/service/panel/panel.go","lineNumber":387,"sourceCode":"\t\treturn \"\", fmt.Errorf(\"download panel updater: unexpected HTTP %d\", resp.StatusCode)\n\t}\n\n\tfile, err := os.CreateTemp(\"\", \"3x-ui-update-*.sh\")\n\tif err != nil {\n\t\treturn \"\", err\n\t}\n\tpath := file.Name()\n\tok := false\n\tdefer func() {\n\t\t_ = file.Close()\n\t\tif !ok {\n\t\t\t_ = os.Remove(path)\n\t\t}\n\t}()\n\n\tn, err := io.Copy(file, io.LimitReader(resp.Body, maxPanelUpdaterBytes+1))\n\tif err != nil {\n\t\treturn \"\", fmt.Errorf(\"write panel updater: %w\", err)\n\t}\n\tif n == 0 {\n\t\treturn \"\", fmt.Errorf(\"panel updater download is empty\")\n\t}\n\tif n > maxPanelUpdaterBytes {\n\t\treturn \"\", fmt.Errorf(\"panel updater exceeds %d bytes\", maxPanelUpdaterBytes)\n\t}\n\tif err := file.Chmod(0o700); err != nil {\n\t\treturn \"\", err\n\t}\n\tok = true\n\treturn path, nil\n}\n\nfunc fetchLatestPanelVersion() (string, error) {\n\trelease, err := fetchPanelRelease(\"\")\n\tif err != nil {\n\t\treturn \"\", err","sourceCodeStart":369,"sourceCodeEnd":405,"githubUrl":"https://github.com/MHSanaei/3x-ui/blob/ad32144c42455696ea9f14e12168beac3e25f5d2/internal/web/service/panel/panel.go#L369-L405","documentation":"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.","triggerScenarios":"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).","commonSituations":"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.","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"],"exampleFix":"// before: opaque failure when /tmp is full\n// (fix at the ops level)\n// after: point TMPDIR at a writable disk before updating\n// export TMPDIR=/var/tmp && x-ui update-panel\n","handlingStrategy":"try-catch","validationCode":"// Before triggering the update, check the temp filesystem\nif f, err := os.CreateTemp(\"\", \"probe\"); err != nil {\n    log.Printf(\"temp dir unusable: %v\", err)\n} else {\n    f.Close()\n    os.Remove(f.Name())\n}\n","typeGuard":null,"tryCatchPattern":"path, err := panel.DownloadPanelUpdater()\nif err != nil {\n    var pathErr *os.PathError\n    if errors.As(err, &pathErr) && errors.Is(pathErr.Err, syscall.ENOSPC) {\n        // surface 'disk full' to the operator, not a raw stack\n    }\n    return fmt.Errorf(\"panel update download failed: %w\", err)\n}\n","preventionTips":["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"],"tags":["io","download","updater","filesystem"],"backgroundTag":null,"analyzedSha":"ad32144c42455696ea9f14e12168beac3e25f5d2","analyzedAt":"2026-08-15T11:13:23.905Z","schemaVersion":2},"datasetVersion":"2026-08-15T22:17:37.221Z"}