{"record":{"id":"4220ccda3a6dd710","repo":"AlistGo/alist","slug":"read-on-closed-file-4220cc","errorCode":null,"errorMessage":"read on closed file","messagePattern":"read on closed file","errorType":"exception","errorClass":null,"httpStatus":null,"severity":"error","filePath":"drivers/mega/util.go","lineNumber":48,"sourceCode":"\tvar chunk []byte\n\terr = utils.Retry(3, time.Second, func() (err error) {\n\t\tchunk, err = oo.d.DownloadChunk(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, err := oo.d.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\n\t\t}","sourceCodeStart":30,"sourceCodeEnd":66,"githubUrl":"https://github.com/AlistGo/alist/blob/843d9dc8149126976b2625911e45a4d3ffd6f2f5/drivers/mega/util.go#L30-L66","documentation":"Returned by openObject.Read (the io.Reader the Mega driver hands out for downloads) when Read is called after Close. The openObject tracks a closed flag under a mutex; once Close has run, any further Read is a usage error, equivalent to reading a closed os.File.","triggerScenarios":"Calling Read on a Mega download stream after Close — typically a caller that closes the reader on error/timeout and then retries or logs by draining the body, or concurrent goroutines where one closes while another still reads (the mutex serializes them but cannot prevent close-then-read ordering).","commonSituations":"HTTP handlers that copy the reader with a context timeout, hit the timeout, close the body, then an error path or a retry wrapper attempts another Read; preview/streaming features that double-consume the same reader; defer-ordered code where Close executes before a late Read in another goroutine.","solutions":["Audit the consumer of the download stream: never Read after Close; treat Close as terminal.","On retries, obtain a fresh reader from the driver instead of reusing the closed one.","Where multiple goroutines touch the reader, funnel all Read/Close calls through one owner (e.g. io.Copy in a single goroutine) or synchronize with a WaitGroup before closing.","Optionally use errors.Is/strings matching on 'read on closed file' to detect misuse in logs."],"exampleFix":"// before\nn, err := body.Read(buf) // panics logic later, body already closed by timeout handler\n\n// after\n// single owner pattern\ngo func() {\n  defer body.Close()\n  _, copyErr := io.Copy(dst, body)\n  if copyErr != nil { log.Warn(\"mega download aborted: \", copyErr) }\n}()","handlingStrategy":"validation","validationCode":"// Before reading in each consumer goroutine, route through a single owner:\n// creator guarantees Read and Close happen in one goroutine (io.Copy)","typeGuard":null,"tryCatchPattern":"if n, err := r.Read(buf); err != nil {\n  if strings.Contains(err.Error(), \"read on closed file\") {\n    // misuse: reader was closed; open a new one instead of retrying\n    fresh, oerr := driver.Get(ctx, path) // re-open\n    if oerr != nil { return oerr }\n    r = fresh\n  }\n}","preventionTips":["Treat Close as terminal: never Read afterwards.","Use defer body.Close() in the same function/goroutine that reads.","On any read error, close and re-open a fresh reader rather than reusing.","Keep one owner goroutine per reader; coordinate close with WaitGroups if sharing."],"tags":["mega","io","read-after-close","concurrency","go"],"backgroundTag":null,"analyzedSha":"843d9dc8149126976b2625911e45a4d3ffd6f2f5","analyzedAt":"2026-08-15T12:14:11.722Z","schemaVersion":2},"datasetVersion":"2026-08-15T22:17:37.221Z"}