{"record":{"id":"240b11e4b43fa971","repo":"AlistGo/alist","slug":"read-on-closed-file","errorCode":null,"errorMessage":"read on closed file","messagePattern":"read on closed file","errorType":"exception","errorClass":null,"httpStatus":null,"severity":"error","filePath":"drivers/halalcloud/util.go","lineNumber":305,"sourceCode":"\tvar chunk []byte\n\terr = utils.Retry(3, time.Second, func() (err error) {\n\t\tchunk, err = getRawFiles(oo.d[oo.id])\n\t\treturn err\n\t})\n\tif err != nil {\n\t\treturn err\n\t}\n\too.id++\n\too.chunk = &chunk\n\treturn nil\n}\n\n// Read reads up to len(p) bytes into p.\nfunc (oo *openObject) Read(p []byte) (n int, err error) {\n\too.mu.Lock()\n\tdefer oo.mu.Unlock()\n\tif oo.closed {\n\t\treturn 0, fmt.Errorf(\"read on closed file\")\n\t}\n\t// Skip data at the start if requested\n\tfor oo.skip > 0 {\n\t\t//size := 1024 * 1024\n\t\t_, size, err := oo.ChunkLocation(oo.id)\n\t\tif err != nil {\n\t\t\treturn 0, err\n\t\t}\n\t\tif oo.skip < int64(size) {\n\t\t\tbreak\n\t\t}\n\t\too.id++\n\t\too.skip -= int64(size)\n\t}\n\tif len(*oo.chunk) == 0 {\n\t\terr = oo.getChunk(oo.ctx)\n\t\tif err != nil {\n\t\t\treturn 0, err","sourceCodeStart":287,"sourceCodeEnd":323,"githubUrl":"https://github.com/AlistGo/alist/blob/843d9dc8149126976b2625911e45a4d3ffd6f2f5/drivers/halalcloud/util.go#L287-L323","documentation":"Returned by openObject.Read when the caller reads from a stream whose Close was already called. The mutex-guarded closed flag makes this deterministic: any Read after Close yields this error rather than undefined behavior. It mirrors io.ErrClosed semantics but as a distinct sentinel-style message.","triggerScenarios":"A goroutine still draining the reader while another calls Close (e.g. ranged readers closed early on cancel); retry logic reusing a closed stream; http.Response bodies read after the request context was cancelled and cleanup ran.","commonSituations":"io.Copy with a cancel mid-flight where cleanup races the final Read; wrapping the stream in a pool/buffer that recycles on Close while a consumer still holds it; video seeking implementations that close the old range reader before a pending Read finishes.","solutions":["Ensure single ownership: only the goroutine that finishes reading calls Close (use sync.Once or defer in the same scope as the read loop).","Check the closed state (or track it in your wrapper) before issuing Read.","Coordinate cancellation: cancel the context, wait for the reader goroutine to observe EOF/err, then Close.","If multiplexing, wrap openObject in your own refcounted ReadCloser that closes the underlying one last."],"exampleFix":"// before\n go func() { io.Copy(dst, r) }()\n r.Close() // races the copy\n\n// after\n var wg sync.WaitGroup\n wg.Add(1)\n go func() { defer wg.Done(); io.Copy(dst, r) }()\n wg.Wait()\n r.Close()","handlingStrategy":"validation","validationCode":"// ensure reads finish before close in the owning goroutine\nvar wg sync.WaitGroup\nwg.Add(1)\ngo func() { defer wg.Done(); io.Copy(dst, stream) }()\n// ...later\nwg.Wait()\nstream.Close()","typeGuard":null,"tryCatchPattern":"n, err := stream.Read(buf)\nif err != nil && strings.Contains(err.Error(), \"read on closed file\") {\n    // ownership bug in caller: reopen the stream, do not retry the read\n    stream, _ = reopen(ctx)\n    n, err = stream.Read(buf)\n}","preventionTips":["One owner goroutine for read+close lifecycle","Close only after EOF or error","Use errgroup to join reader goroutines before cleanup"],"tags":["halalcloud","closed-stream","concurrency","io"],"backgroundTag":null,"analyzedSha":"843d9dc8149126976b2625911e45a4d3ffd6f2f5","analyzedAt":"2026-08-15T12:14:11.722Z","schemaVersion":2},"datasetVersion":"2026-08-15T22:17:37.221Z"}