Skip to content

Fix crash on Azure stream chunks without delta - #29

Merged
runephilosof-abtion merged 4 commits into
mainfrom
fix/azure-stream-content-filter-crash
Sep 28, 2026
Merged

runephilosof-abtion merged 4 commits into
mainfrom
fix/azure-stream-content-filter-crash

Conversation

@runephilosof-abtion

@runephilosof-abtion runephilosof-abtion commented Sep 28, 2026 •

Copy link
Copy Markdown
Member

Problem

Production web containers are crash-looping with:

TypeError: Cannot destructure property 'content' of 'part.choices[0].delta' as it is undefined.
    at /dist/openai/openai.service.js:294:37

With AI_PROVIDER=openai-azure, Azure sometimes sends stream chunks where choices[0] has no delta, typically content-filter chunks like { choices: [{ index: 0, finish_reason: null, content_filter_results: {} }] }. The throw happened inside a new Promise(async ...) executor. That made it an unhandled rejection, which killed the Node process and every in-flight chat on the pod.

Changes

  • Read the content with part.choices?.[0]?.delta?.content and skip the chunk when it's null/undefined. Before, content: null would have added the text "null" to the answer.
  • Replace the new Promise(async ...) with a private forwardCompletionStream method that is started without being awaited and never rejects:
    • If the stream fails, it logs the error and errors the observable with a generic message. The @Sse('/answer_stream') endpoint is the only consumer, and NestJS already turns that into an SSE error event for that one client. A partial answer is not saved.
    • Token counting and completeCb are now inside a try/catch, and completeCb is awaited, so errors there are logged. Before, it was fire-and-forget, which was a second unhandled-rejection path (e.g. when saving the session message fails).
  • Add optional chaining to res.choices[0].message.content in the non-streaming completion. An empty (e.g. filtered) response now defaults to '', the same as the streaming path.
  • Add openai.service.spec.ts with a mocked Azure client. It covers the content-filter chunk, a stream that fails mid-answer, and a rejecting completion callback. On the old code, the first test fails with the same TypeError as production.

Why no global unhandledRejection handler

It wouldn't have prevented this crash. Sentry v7 runs requests inside a Node domain, and unhandled rejections inside a domain go to domain.emit('error') instead of process.on('unhandledRejection'). That's the "Emitted 'error' event on Domain instance" in the trace. Only an uncaughtException handler would catch it, and keeping the process alive after those is unsafe.

Verification

  • yarn build passes
  • yarn test: 7 suites / 16 tests pass
  • ESLint + Prettier clean on changed files

🤖 Generated with Claude Code

Copilot AI lite review requested due to automatic review settings September 28, 2026 09:50

Copilot AI left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Copilot review overview

🔵 Needs a closer look

Moderate issues remain in tokenization error handling and empty non-streaming responses.

Review effort: Lite
Findings: None

What changed in this PR

Fixes Azure OpenAI stream crashes caused by chunks without delta and improves completion error handling.

Changes:

  • Safely skips malformed/content-filter stream chunks.
  • Handles stream and callback failures.
  • Adds focused tests and Jest path mapping.
File Summary
test/​jest-e2e.json Adds src module mapping for tests.
src/​openai/​openai.service.ts Hardens streaming and non-streaming completion handling.
src/​openai/​openai.service.spec.ts Tests filtered chunks, stream failures, and callback failures.
src/​knowledgebase/​chatbot/​openaiChatbotService.spec.ts Updates chatbot message fixtures.

💡 Add a code-review agent skill or configure MCP servers for context-aware, tailored reviews. Learn more in the docs.

runephilosof-abtion and others added 2 commits September 28, 2026 12:39
Azure OpenAI sends stream chunks where choices[0] has no delta (e.g.
content filter results). Destructuring delta threw inside an async
Promise executor, which became an unhandled rejection and killed the
process along with every in-flight chat.

Read content with optional chaining and skip empty chunks. Move the
stream loop into a method that never rejects: a stream failure errors
the observable for that one answer, and completion callback failures
are logged.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Move getTokenCount inside the guarded block so a tokenizer failure
cannot become an unhandled rejection, and default an empty
non-streaming response to '' to match the streaming path.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Copilot AI review requested due to automatic review settings September 28, 2026 10:41
@runephilosof-abtion
runephilosof-abtion force-pushed the fix/azure-stream-content-filter-crash branch from cc59c80 to 26a9351 Compare September 28, 2026 10:41

Copilot AI left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Copilot review overview

🔵 Needs a closer look

Correct the ineffective APIError.isPrototypeOf(error) check before approval.

Review effort: Lite
Findings: None

APIError.isPrototypeOf(error) is always false for instances, so API
errors were never logged. Use instanceof and log the parsed error body,
since APIError has no data field.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Copilot AI review requested due to automatic review settings September 28, 2026 10:46

Copilot AI left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Copilot review overview

🔵 Needs a closer look

Add regression coverage for non-streaming filtered responses returning an empty result.

Review effort: Lite
Findings: None

Previously missed (1)

In code that hasn't changed since last review

Medium severity Add regression test for empty choices fallback

src/​openai/​openai.service.ts:347

This fallback is untested: a non-streaming filtered response with choices: [] is the case this optional chaining is intended to handle. Please add a regression test for getChatGptCompletion that asserts it returns response: ''; otherwise this separate crash fix can be accidentally removed without a test failure.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Copilot AI review requested due to automatic review settings September 28, 2026 10:51

Copilot AI left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Copilot review overview

🟢 Approval recommended

The reported crash paths are addressed with focused regression coverage.

Review effort: Lite
Findings: None

@runephilosof-abtion runephilosof-abtion left a comment

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

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

LGTM

@runephilosof-abtion
runephilosof-abtion merged commit 58f14a8 into main Sep 28, 2026
4 checks passed
@runephilosof-abtion
runephilosof-abtion deleted the fix/azure-stream-content-filter-crash branch September 28, 2026 11:22
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.

2 participants