{"record":{"id":"b055fa357d02be75","repo":"moeru-ai/airi","slug":"timeout-must-be-a-positive-finite-number","errorCode":null,"errorMessage":"timeout must be a positive finite number","messagePattern":"timeout must be a positive finite number","errorType":"validation","errorClass":"Error","httpStatus":null,"severity":"error","filePath":"packages/electron-screen-capture/src/main/index.ts","lineNumber":226,"sourceCode":"\n  defineInvokeHandler(context, screenCapture.getSources, async (sourcesOptions) => {\n    // NOTICE(@nekomeowww): In probability of 9/10, the window thumbnail is purely empty or black, sources printed and\n    // nothing is returned from the desktopCapturer API.\n    // NOTICE(@sumimakito): Not only thumbnail is empty, the appIcon could be empty as well with nothing returned.\n    // REVIEW(@sumimakito): This has nothing to do with out side, probably related to Electron Bug, you can\n    // read more here https://github.com/electron/electron/issues/44504\n    const sources = await desktopCapturer.getSources(sourcesOptions)\n    return sources.map(source => toSerializableDesktopCapturerSource(source))\n  })\n\n  defineInvokeHandler(context, screenCapture.setSource, async (request, eventaOptions) => {\n    // FIXME: Would be better if `onlySameWindow` in `createContext` also filters out invocations here.\n    if (window.webContents.id !== eventaOptions?.raw.ipcMainEvent.sender.id)\n      return\n\n    const { timeout } = request\n    if (typeof timeout === 'number' && (timeout <= 0 || !Number.isFinite(timeout) || Number.isNaN(timeout))) {\n      throw new Error('timeout must be a positive finite number')\n    }\n\n    await setSourceMutex.acquire()\n    log.withFields({ windowId, windowTitle: tryWindowTitle(window, windowTitle) }).debug('setSourceMutex acquired')\n\n    clearTimeout(setSourceMutexTimeoutHandle)\n    const handle = nanoid()\n    setSourceMutexTimeoutHandle = undefined\n    screenCaptureSourceMutexHandle = handle\n\n    try {\n      session.setDisplayMediaRequestHandler(async (_request, callback) => {\n        const sources = await desktopCapturer.getSources(request.options)\n        const source = sources.find(source => source.id === request.sourceId)\n        if (!source) {\n          throw new Error(`Source with id ${request.sourceId} not found.`)\n        }\n","sourceCodeStart":208,"sourceCodeEnd":244,"githubUrl":"https://github.com/moeru-ai/airi/blob/677329427f32468c74b17f3ec47eeca4e05bec65/packages/electron-screen-capture/src/main/index.ts#L208-L244","documentation":"Thrown by the screenCapture.setSource eventa invoke handler in the Electron main process when the optional timeout field of the request is a number but fails validation: it must be > 0, finite, and not NaN. The timeout controls how long the main process holds setSourceMutex while waiting for the renderer's getUserMedia/getDisplayMedia call to hit session.setDisplayMediaRequestHandler (default 5000 ms when omitted). The check runs before the mutex is acquired, so no state leaks when it throws. Note that a non-number value (e.g. undefined) is allowed and falls back to the 5s default.","triggerScenarios":"Renderer calls screenCapture.setSource({ sourceId, options, timeout }) with timeout = 0, a negative number, Infinity, -Infinity, or NaN. Typical causes: computing timeout from a subtraction that yields 0 or a negative drift, parsing it from user input or settings JSON without validation, or passing Number.POSITIVE_INFINITY for 'no timeout'.","commonSituations":"A settings UI exposes a capture timeout slider that can reach 0; a config file written by an older app version contains 0; timeout is derived from a countdown that expired; JSON.parse turns 'Infinity' string handling into NaN via Number(...) on bad input.","solutions":["Pass a positive finite number of milliseconds, e.g. timeout: 10_000, or omit the field entirely to use the built-in 5000 ms default.","If the value comes from user config, clamp it before sending: Math.min(Math.max(Number(raw), 100), 60_000).","Search the renderer code for the setSource call site and log/inspect the exact timeout value being serialized over IPC.","If you intended 'wait forever', do not use Infinity; pick a large finite bound and release via screenCapture.resetSource(handle) when getDisplayMedia completes."],"exampleFix":"// before\nawait invoke(screenCapture.setSource, {\n  sourceId,\n  options,\n  timeout: remainingMs, // remainingMs can be 0 or negative after a countdown\n})\n\n// after\nconst timeout = Number.isFinite(remainingMs) && remainingMs > 0\n  ? Math.min(remainingMs, 60_000)\n  : undefined // falls back to the 5000 ms default\nawait invoke(screenCapture.setSource, { sourceId, options, timeout })","handlingStrategy":"validation","validationCode":"function normalizeSetSourceTimeout(raw: unknown): number | undefined {\n  if (raw == null) return undefined // host default: 5000 ms\n  const n = Number(raw)\n  if (!Number.isFinite(n) || n <= 0) return undefined // drop invalid, use default\n  return Math.min(n, 60_000)\n}\n\nconst timeout = normalizeSetSourceTimeout(settings.captureTimeout)","typeGuard":"function isPositiveFiniteTimeout(v: unknown): v is number {\n  return typeof v === 'number' && Number.isFinite(v) && v > 0\n}","tryCatchPattern":"try {\n  await invoke(screenCapture.setSource, { sourceId, options, timeout })\n} catch (error) {\n  if (errorMessageFrom(error).includes('timeout must be a positive finite number')) {\n    // config bug: fix the stored timeout and retry with the default\n    await invoke(screenCapture.setSource, { sourceId, options })\n  } else throw error\n}","preventionTips":["Never compute timeout from a subtraction without clamping; expired countdowns yield 0 or negatives.","Validate user-configurable timeouts at the settings boundary (min 100 ms, max 60 s), not at the IPC boundary.","Remember undefined is valid and selects the 5000 ms default — omit rather than pass Infinity."],"tags":["electron","ipc","screen-capture","validation","timeout"],"backgroundTag":"invalid-argument-value","analyzedSha":"677329427f32468c74b17f3ec47eeca4e05bec65","analyzedAt":"2026-08-18T17:29:58.153Z","contentChangedAt":"2026-08-18T17:29:58.153Z","schemaVersion":2},"datasetVersion":"2026-09-23T08:17:48.524Z"}