Skip to content

Conversation

@codewarnab
Copy link

@codewarnab codewarnab commented Dec 30, 2025

This PR addresses a runtime error: TypeError: Cannot read properties of undefined (reading 'text') in processUIMessageStream.

Issue

The issue occurs when text-delta, reasoning-delta, or tool-input-delta chunks are received before their corresponding start chunks (e.g., text-start). This can happen in malformed streams, network issues causing packet reordering, or specific backend behaviors where a delta is emitted immediately without a start frame.

Previously, processUIMessageStream assumed that a start part (initialized in activeTextParts, etc.) always existed when a delta arrived. When it didn't, accessing properties like textPart.text caused a crash.

Fix

This change adds explicit null checks for the active parts (textPart, reasoningPart, partialToolCall). If a delta is received for a missing part, it now throws a descriptive error:

"Received text-delta for missing text part with ID "text-1". Ensure a "text-start" chunk is sent before any "text-delta" chunks."

This descriptive error aids in debugging the actual upstream issue (broken stream) rather than crashing the UI with an opaque TypeError.

Changes

  • packages/ai/src/ui/process-ui-message-stream.ts: Added guarding if (part == null) checks in text-delta, reasoning-delta, and tool-input-delta cases.
  • packages/ai/src/ui/process-ui-message-stream.test.ts:
    • Added test cases simulating malformed streams (delta without start) for text, reasoning, and tool-inputs.
    • Verified that specific descriptive errors are thrown.
    • Updated test harness consumeStream to properly propagate errors for validation.

Copilot AI review requested due to automatic review settings January 4, 2026 19:02
Copy link
Contributor

Copilot AI left a comment

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 adds defensive null checks to prevent runtime errors when processing malformed UI message streams. The fix addresses cases where delta or end chunks are received before their corresponding start chunks.

Summary

The PR adds null checks for text-delta, text-end, reasoning-delta, reasoning-end, and tool-input-delta chunk types to prevent crashes when start chunks are missing. Instead of crashing with a generic TypeError, the code now throws descriptive errors that help identify the upstream issue.

Key Changes

  • Added null checks with descriptive error messages for delta and end chunks in stream processing
  • Added test cases to verify error handling for malformed streams (delta without start)
  • Minor formatting improvements to type definitions

Reviewed changes

Copilot reviewed 2 out of 2 changed files in this pull request and generated 5 comments.

File Description
packages/ai/src/ui/process-ui-message-stream.ts Added null checks for text, reasoning, and tool-input parts with descriptive error messages; minor formatting improvements
packages/ai/src/ui/process-ui-message-stream.test.ts Added test cases for malformed stream scenarios where delta chunks are received without corresponding start chunks

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

Comment on lines +375 to +380
if (reasoningPart == null) {
throw new Error(
`Received reasoning-end for missing reasoning part with ID "${chunk.id}". ` +
`Ensure a "reasoning-start" chunk is sent before any "reasoning-end" chunks.`,
);
}
Copy link

Copilot AI Jan 4, 2026

Choose a reason for hiding this comment

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

Null checks were added for reasoning-end chunks (lines 375-380) but there are no corresponding test cases. For consistency with the delta chunk tests, consider adding tests that verify the error is thrown when reasoning-end is received without a preceding reasoning-start chunk.

Copilot uses AI. Check for mistakes.
Comment on lines +240 to +256
await expect(
consumeStream({
stream: processUIMessageStream({
stream,
runUpdateMessageJob,
onError: error => {
throw error;
},
}),
onError: error => {
throw error;
},
}),
).rejects.toThrow(
'Received text-delta for missing text part with ID "text-1". ' +
'Ensure a "text-start" chunk is sent before any "text-delta" chunks.',
);
Copy link

Copilot AI Jan 4, 2026

Choose a reason for hiding this comment

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

The test expects consumeStream to propagate the error, but the consumeStream utility catches errors and only calls the onError callback without re-throwing. This means the test will not actually catch the error as expected.

To fix this, the onError callback in consumeStream needs to re-throw the error after calling the callback, or the test should be restructured to collect errors in an array like the existing "errors" test does (see line 177-225).

Copilot uses AI. Check for mistakes.
Comment on lines +271 to +287
await expect(
consumeStream({
stream: processUIMessageStream({
stream,
runUpdateMessageJob,
onError: error => {
throw error;
},
}),
onError: error => {
throw error;
},
}),
).rejects.toThrow(
'Received reasoning-delta for missing reasoning part with ID "reasoning-1". ' +
'Ensure a "reasoning-start" chunk is sent before any "reasoning-delta" chunks.',
);
Copy link

Copilot AI Jan 4, 2026

Choose a reason for hiding this comment

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

The test expects consumeStream to propagate the error, but the consumeStream utility catches errors and only calls the onError callback without re-throwing. This means the test will not actually catch the error as expected.

To fix this, the onError callback in consumeStream needs to re-throw the error after calling the callback, or the test should be restructured to collect errors in an array like the existing "errors" test does (see line 177-225).

Copilot uses AI. Check for mistakes.
Comment on lines +306 to +322
await expect(
consumeStream({
stream: processUIMessageStream({
stream,
runUpdateMessageJob,
onError: error => {
throw error;
},
}),
onError: error => {
throw error;
},
}),
).rejects.toThrow(
'Received tool-input-delta for missing tool call with ID "tool-1". ' +
'Ensure a "tool-input-start" chunk is sent before any "tool-input-delta" chunks.',
);
Copy link

Copilot AI Jan 4, 2026

Choose a reason for hiding this comment

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

The test expects consumeStream to propagate the error, but the consumeStream utility catches errors and only calls the onError callback without re-throwing. This means the test will not actually catch the error as expected.

To fix this, the onError callback in consumeStream needs to re-throw the error after calling the callback, or the test should be restructured to collect errors in an array like the existing "errors" test does (see line 177-225).

Copilot uses AI. Check for mistakes.
Comment on lines +331 to +336
if (textPart == null) {
throw new Error(
`Received text-end for missing text part with ID "${chunk.id}". ` +
`Ensure a "text-start" chunk is sent before any "text-end" chunks.`,
);
}
Copy link

Copilot AI Jan 4, 2026

Choose a reason for hiding this comment

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

Null checks were added for text-end chunks (lines 331-336) but there are no corresponding test cases. For consistency with the delta chunk tests, consider adding tests that verify the error is thrown when text-end is received without a preceding text-start chunk.

Copilot uses AI. Check for mistakes.
lastMessage: undefined,
});

await expect(
Copy link
Contributor

Choose a reason for hiding this comment

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

Malformed stream tests incorrectly expect promise rejection via .rejects.toThrow() instead of collecting errors in callback

Fix on Vercel

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.

1 participant