{"record":{"id":"5e02133758f540d2","repo":"router-for-me/CLIProxyAPI","slug":"invalid-cursor-offset","errorCode":null,"errorMessage":"invalid cursor offset","messagePattern":"invalid cursor offset","errorType":"http","errorClass":null,"httpStatus":500,"severity":"error","filePath":"internal/api/handlers/management/logs.go","lineNumber":980,"sourceCode":"\tif errRel != nil {\n\t\treturn \"\", fmt.Errorf(\"resolve log file: %w\", errRel)\n\t}\n\tif rel == \".\" || strings.HasPrefix(rel, \"..\"+string(os.PathSeparator)) || rel == \"..\" || filepath.IsAbs(rel) {\n\t\treturn \"\", fmt.Errorf(\"invalid log file\")\n\t}\n\treturn fullPath, nil\n}\n\nfunc newLogCursor(path string, offset, latest int64) (string, error) {\n\tinfo, errStat := os.Stat(path)\n\tif errStat != nil {\n\t\treturn \"\", errStat\n\t}\n\tif info.IsDir() {\n\t\treturn \"\", fmt.Errorf(\"invalid log file\")\n\t}\n\tif offset < 0 || offset > info.Size() {\n\t\treturn \"\", fmt.Errorf(\"invalid cursor offset\")\n\t}\n\tfingerprintCursor := logCursor{\n\t\tOffset: offset,\n\t\tSize:   info.Size(),\n\t}\n\tfingerprint, errFingerprint := logFileFingerprint(path, cursorFingerprintBoundary(fingerprintCursor))\n\tif errFingerprint != nil {\n\t\treturn \"\", errFingerprint\n\t}\n\treturn encodeLogCursor(logCursor{\n\t\tVersion:         logCursorVersion,\n\t\tFile:            filepath.Base(path),\n\t\tOffset:          offset,\n\t\tSize:            info.Size(),\n\t\tModTime:         info.ModTime().Unix(),\n\t\tModTimeUnixNano: info.ModTime().UnixNano(),\n\t\tLatestTimestamp: latest,\n\t\tFingerprint:     fingerprint,","sourceCodeStart":962,"sourceCodeEnd":998,"githubUrl":"https://github.com/router-for-me/CLIProxyAPI/blob/78f0c4079e3e6273d65d03b5549cffc898703264/internal/api/handlers/management/logs.go#L962-L998","documentation":"Returned by newLogCursor (logs.go:980) when the requested cursor offset is negative or larger than the current size of the log file. Cursors embed a byte offset; before issuing a new cursor the server verifies the offset still falls within [0, fileSize]. If the file shrank since the offset was obtained (rotation, truncation), the check fails.","triggerScenarios":"Calling the log pagination flow with an offset derived from a previous read after the log file was truncated or rotated to a smaller file; passing a negative offset programmatically; two clients racing where one rotates the file between the other's read and continuation.","commonSituations":"External log rotation (logrotate with copytruncate) shrinks the active log while a dashboard keeps an old cursor; a long-lived tail client resumes after disk cleanup truncated logs.","solutions":["Drop the cursor and restart the log read from offset 0 (or from the beginning without a cursor) to resynchronize with the current file","If you manage offsets yourself, clamp them to the current file size before requesting a cursor: offset = min(offset, fileSize)","Avoid copytruncate-style rotation for this log, or rotate by rename so the server re-detects the file via its allowed-name checks","Verify the cursor you replay actually came from the most recent response, not from an earlier session"],"exampleFix":"// before: replaying a stale offset after the file shrank\ncursor, err := newLogCursor(path, oldOffset, latest)\n\n// after: clamp to current size before creating the cursor\ninfo, err := os.Stat(path)\nif err != nil {\n\treturn err\n}\nif oldOffset > info.Size() {\n\toldOffset = 0 // file rotated/truncated; restart\n}\ncursor, err := newLogCursor(path, oldOffset, latest)","handlingStrategy":"validation","validationCode":"// Clamp the offset to the current file size before requesting a cursor.\ninfo, err := os.Stat(path)\nif err != nil {\n\treturn err\n}\nif offset < 0 || offset > info.Size() {\n\toffset = 0 // file rotated/truncated; restart from the beginning\n}","typeGuard":null,"tryCatchPattern":null,"preventionTips":["Treat any cursor failure after rotation as a signal to restart pagination from offset 0","Prefer rename-based log rotation over truncation so file sizes never shrink under live offsets","Always carry the cursor returned by the latest response, never an older one"],"tags":["logs","pagination","offset","rotation","management-api"],"backgroundTag":null,"analyzedSha":"78f0c4079e3e6273d65d03b5549cffc898703264","analyzedAt":"2026-08-15T12:26:37.444Z","schemaVersion":2},"datasetVersion":"2026-08-15T17:31:12.345Z"}