Skip to content

📸 fix: Snapshot Options to Prevent Mid-Await Client Disposal Crash#12398

Merged
danny-avila merged 2 commits into
devfrom
claude/awesome-meninsky
Mar 25, 2026
Merged

📸 fix: Snapshot Options to Prevent Mid-Await Client Disposal Crash#12398
danny-avila merged 2 commits into
devfrom
claude/awesome-meninsky

Conversation

@danny-avila

@danny-avila danny-avila commented Mar 25, 2026

Copy link
Copy Markdown
Owner

Summary

  • Fixes an uncaught TypeError: Cannot read properties of null (reading 'endpoint') that crashes the Node process when a user exhausts their token balance
  • Root cause: race condition where sendMessage() fires an async userMessagePromise via saveMessageToDatabase() (without awaiting it), then checkBalance() throws, and the error handler calls disposeClient() which sets client.options = null — while the DB save promise is still suspended at an await
  • Fix: snapshot this.options into a local const options before any await in saveMessageToDatabase(), then use the local reference throughout. disposeClient sets client.options = null (nulling the property on the client), but the local variable retains its reference to the original object — immune to external mutation
  • Also adds .catch() on userMessagePromise in sendMessage() to prevent unhandled rejections if the promise rejects after checkBalance throws (since no one awaits it on the error path)
  • Adds two regression tests: one for the null-guard at entry, one that simulates mid-await disposal and verifies no crash

Test plan

  • saveMessageToDatabase returns {} early when this.options is null at entry
  • saveMessageToDatabase completes without TypeError when this.options is nullified mid-await (snapshot protects all post-await accesses)
  • All 60 BaseClient tests pass (58 existing + 2 new)
  • Manual: enable balance with low limits, exhaust token balance mid-conversation, verify server does not crash

…disposal

Add null guard in saveMessageToDatabase to handle the case where
disposeClient nullifies this.options while a userMessagePromise is
still pending from a prior async save operation.
Copilot AI review requested due to automatic review settings March 25, 2026 17:26

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

This PR aims to prevent a Node process crash caused by a race condition where disposeClient() nulls client.options while an in-flight saveMessageToDatabase() async operation is still running (triggering TypeError: Cannot read properties of null (reading 'endpoint')).

Changes:

  • Add an early null-guard in BaseClient.saveMessageToDatabase() to skip saving when the client has already been disposed.
  • Emit a warning log when a post-disposal save is attempted.

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

Comment thread api/app/clients/BaseClient.js Outdated
Comment on lines +730 to +733
if (!this.options) {
logger.warn('[BaseClient] saveMessageToDatabase called after client disposal, skipping save');
return {};
}

Copilot AI Mar 25, 2026

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The null guard only protects the beginning of the method. saveMessageToDatabase still dereferences this.options.endpoint/this.options.endpointType after an await (e.g., when building fieldsToKeep), so disposeClient() can still set this.options = null mid-flight and trigger the same TypeError. Fix by snapshotting the needed values from this.options into locals before the first await and avoid reading this.options after awaits, or re-check this.options after await db.saveMessage(...) and return early (e.g., skip convo save) if disposed.

Copilot uses AI. Check for mistakes.
Comment thread api/app/clients/BaseClient.js Outdated
Comment on lines +730 to +733
if (!this.options) {
logger.warn('[BaseClient] saveMessageToDatabase called after client disposal, skipping save');
return {};
}

Copilot AI Mar 25, 2026

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Add a regression test that simulates disposal during the await inside saveMessageToDatabase (e.g., delay db.saveMessage/db.saveConvo, set client.options = null before the promise resolves) and asserts the method resolves without throwing. This will cover the race described in the PR and prevent future reintroductions.

Copilot uses AI. Check for mistakes.
The original guard at function entry was insufficient — this.options is
always valid at entry. The crash occurs after the first await
(db.saveMessage) when disposeClient nullifies client.options while the
promise is suspended.

Fix: capture this.options into a local const before any await. The local
reference is immune to client.options = null set by disposeClient.

Also add .catch on userMessagePromise in sendMessage to prevent
unhandled rejections when checkBalance throws before the promise is
awaited, and add two regression tests.
@danny-avila danny-avila changed the title 🐛 fix: Prevent crash when token balance exhaustion races with client disposal 📸 fix: Snapshot Options to Prevent Mid-Await Client Disposal Crash Mar 25, 2026
@danny-avila danny-avila changed the base branch from main to dev March 25, 2026 18:17
@danny-avila danny-avila merged commit f277b32 into dev Mar 25, 2026
9 checks passed
@danny-avila danny-avila deleted the claude/awesome-meninsky branch March 25, 2026 18:18
berry-13 pushed a commit that referenced this pull request Mar 26, 2026
…12398)

* 🐛 fix: Prevent crash when token balance exhaustion races with client disposal

Add null guard in saveMessageToDatabase to handle the case where
disposeClient nullifies this.options while a userMessagePromise is
still pending from a prior async save operation.

* 🐛 fix: Snapshot this.options to prevent mid-await disposal crash

The original guard at function entry was insufficient — this.options is
always valid at entry. The crash occurs after the first await
(db.saveMessage) when disposeClient nullifies client.options while the
promise is suspended.

Fix: capture this.options into a local const before any await. The local
reference is immune to client.options = null set by disposeClient.

Also add .catch on userMessagePromise in sendMessage to prevent
unhandled rejections when checkBalance throws before the promise is
awaited, and add two regression tests.
yidianyiko pushed a commit to yidianyiko/LibreChat that referenced this pull request Apr 13, 2026
…anny-avila#12398)

* 🐛 fix: Prevent crash when token balance exhaustion races with client disposal

Add null guard in saveMessageToDatabase to handle the case where
disposeClient nullifies this.options while a userMessagePromise is
still pending from a prior async save operation.

* 🐛 fix: Snapshot this.options to prevent mid-await disposal crash

The original guard at function entry was insufficient — this.options is
always valid at entry. The crash occurs after the first await
(db.saveMessage) when disposeClient nullifies client.options while the
promise is suspended.

Fix: capture this.options into a local const before any await. The local
reference is immune to client.options = null set by disposeClient.

Also add .catch on userMessagePromise in sendMessage to prevent
unhandled rejections when checkBalance throws before the promise is
awaited, and add two regression tests.
jcbartle pushed a commit to jcbartle/LibreChat that referenced this pull request May 11, 2026
…anny-avila#12398)

* 🐛 fix: Prevent crash when token balance exhaustion races with client disposal

Add null guard in saveMessageToDatabase to handle the case where
disposeClient nullifies this.options while a userMessagePromise is
still pending from a prior async save operation.

* 🐛 fix: Snapshot this.options to prevent mid-await disposal crash

The original guard at function entry was insufficient — this.options is
always valid at entry. The crash occurs after the first await
(db.saveMessage) when disposeClient nullifies client.options while the
promise is suspended.

Fix: capture this.options into a local const before any await. The local
reference is immune to client.options = null set by disposeClient.

Also add .catch on userMessagePromise in sendMessage to prevent
unhandled rejections when checkBalance throws before the promise is
awaited, and add two regression tests.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

[Bug]: Token Exhaustion Causes TypeError: Cannot read properties of null (reading 'endpoint')

2 participants