* Sends an LSP request to the server with retry logic for transient errors. * * Checks server health before sending and wraps errors with context. * Automatically retries on "content modified" errors (code -32801) which occur * when servers like rust-analyzer are still indexing. This is
(method: string, params: unknown)
| 353 | * @throws {Error} If server is not healthy or request fails after all retries |
| 354 | */ |
| 355 | async function sendRequest<T>(method: string, params: unknown): Promise<T> { |
| 356 | if (!isHealthy()) { |
| 357 | const error = new Error( |
| 358 | `Cannot send request to LSP server '${name}': server is ${state}` + |
| 359 | `${lastError ? `, last error: ${lastError.message}` : ''}`, |
| 360 | ) |
| 361 | logError(error) |
| 362 | throw error |
| 363 | } |
| 364 | |
| 365 | let lastAttemptError: Error | undefined |
| 366 | |
| 367 | for ( |
| 368 | let attempt = 0; |
| 369 | attempt <= MAX_RETRIES_FOR_TRANSIENT_ERRORS; |
| 370 | attempt++ |
| 371 | ) { |
| 372 | try { |
| 373 | return await client.sendRequest(method, params) |
| 374 | } catch (error) { |
| 375 | lastAttemptError = error as Error |
| 376 | |
| 377 | // Check if this is a transient "content modified" error that we should retry |
| 378 | // This commonly happens with rust-analyzer during initial project indexing. |
| 379 | // We use duck typing instead of instanceof because there may be multiple |
| 380 | // versions of vscode-jsonrpc in the dependency tree (8.2.0 vs 8.2.1). |
| 381 | const errorCode = (error as { code?: number }).code |
| 382 | const isContentModifiedError = |
| 383 | typeof errorCode === 'number' && |
| 384 | errorCode === LSP_ERROR_CONTENT_MODIFIED |
| 385 | |
| 386 | if ( |
| 387 | isContentModifiedError && |
| 388 | attempt < MAX_RETRIES_FOR_TRANSIENT_ERRORS |
| 389 | ) { |
| 390 | const delay = RETRY_BASE_DELAY_MS * Math.pow(2, attempt) |
| 391 | logForDebugging( |
| 392 | `LSP request '${method}' to '${name}' got ContentModified error, ` + |
| 393 | `retrying in ${delay}ms (attempt ${attempt + 1}/${MAX_RETRIES_FOR_TRANSIENT_ERRORS})…`, |
| 394 | ) |
| 395 | await sleep(delay) |
| 396 | continue |
| 397 | } |
| 398 | |
| 399 | // Non-retryable error or max retries exceeded |
| 400 | break |
| 401 | } |
| 402 | } |
| 403 | |
| 404 | // All retries failed or non-retryable error |
| 405 | const requestError = new Error( |
| 406 | `LSP request '${method}' failed for server '${name}': ${lastAttemptError?.message ?? 'unknown error'}`, |
| 407 | ) |
| 408 | logError(requestError) |
| 409 | throw requestError |
| 410 | } |
| 411 | |
| 412 | /** |
nothing calls this directly
no test coverage detected