Skip to content

Fix/nginx ai streaming buffering#1599

Merged
lyubov-voloshko merged 6 commits into
mainfrom
fix/nginx-ai-streaming-buffering
Feb 16, 2026
Merged

Fix/nginx ai streaming buffering#1599
lyubov-voloshko merged 6 commits into
mainfrom
fix/nginx-ai-streaming-buffering

Conversation

@gugu

@gugu gugu commented Feb 14, 2026

Copy link
Copy Markdown
Contributor

No description provided.

gugu and others added 2 commits February 14, 2026 20:32
Nginx was buffering the entire backend response before forwarding it to the
client, preventing SSE streaming on the /ai/v4/request endpoint.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Introduce ApiService as a fetch-based HTTP layer with resource()
factory, streaming via postStream(), and built-in error handling.
Extract AiService from TablesService using streaming responses
with AbortController for cancellation. AI panel now renders
responses progressively as chunks arrive from the backend.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Copilot AI review requested due to automatic review settings February 14, 2026 21:42

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 updates the frontend + nginx proxying to support true incremental AI response streaming (avoiding nginx buffering) and updates the AI panel to consume streamed chunks instead of waiting for a full response.

Changes:

  • Added a new ApiService wrapper around fetch, including postStream() to consume streamed responses.
  • Added AiService that exposes createThread() / sendMessage() streaming APIs for the AI panel.
  • Updated the AI panel component + unit tests to use streaming + AbortController cancellation.
  • Updated nginx /api/ proxy settings to disable buffering/caching for streamed responses.

Reviewed changes

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

Show a summary per file
File Description
frontend/src/app/services/api.service.ts Introduces fetch-based API wrapper with streaming support (postStream) and shared error handling.
frontend/src/app/services/ai.service.ts New AI-specific service that wraps streaming calls and exposes thread/message APIs.
frontend/src/app/components/dashboard/db-table-view/db-table-ai-panel/db-table-ai-panel.component.ts Switches AI panel to streamed rendering + abort-based cancellation.
frontend/src/app/components/dashboard/db-table-view/db-table-ai-panel/db-table-ai-panel.component.spec.ts Updates unit tests to mock async streaming APIs.
frontend/nginx/default.conf Disables proxy buffering/caching for /api/ to allow streaming to flush incrementally.

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

this.angulartics2.eventTrack.next({
action: 'AI panel: message sent successfully',
});
posthog.capture('AI panel: message sent successfully');

Copilot AI Feb 14, 2026

Copy link

Choose a reason for hiding this comment

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

Same issue as createThread(): sendMessage() treats a null stream as a no-op, but ApiService.postStream() uses null for request failures, so the chat can silently stop without displaying an ai-error entry. Add explicit handling for stream === null (or have the service throw) so failures are visible in the panel.

Suggested change
posthog.capture('AI panel: message sent successfully');
posthog.capture('AI panel: message sent successfully');
} else if (stream === null) {
// Handle request failures where sendMessage returns null so they are visible in the UI.
this.messagesChain.push({
type: 'ai-error',
text: 'Failed to send message. Please try again.',
});
this.angulartics2.eventTrack.next({
action: 'AI panel: message send failed',
});
posthog.capture('AI panel: message send failed');

Copilot uses AI. Check for mistakes.
Comment on lines +159 to 168
it('should handle error when creating thread', async () => {
(mockAiService.createThread as ReturnType<typeof vi.fn>).mockRejectedValue('Error message');

component.message = 'User question';
component.createThread();
await component.createThread();

expect(component.messagesChain[1]).toEqual({
type: 'ai-error',
text: 'Error message',
});

Copilot AI Feb 14, 2026

Copy link

Choose a reason for hiding this comment

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

These tests model AiService.createThread() failures as a rejected promise, but the concrete implementation (ApiService.postStream) resolves null on request failure after showing a notification. As a result, the component’s catch path may never run in production unless the service contract is changed to throw. Align the test doubles with the real contract (resolve null) or update AiService/ApiService to reject on errors so the tests reflect production behavior.

Copilot uses AI. Check for mistakes.
Comment on lines +29 to +30
return {
threadId: result.headers.get('X-OpenAI-Thread-ID'),

Copilot AI Feb 14, 2026

Copy link

Choose a reason for hiding this comment

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

AiStreamResponse.threadId is typed as string, but Headers.get('X-OpenAI-Thread-ID') returns string | null. If the header is missing (or blocked by CORS), this will propagate a null thread ID and can break subsequent requests. Consider validating the header and throwing/returning an explicit error, or change the type to string | null and handle that case in the caller.

Suggested change
return {
threadId: result.headers.get('X-OpenAI-Thread-ID'),
const threadId = result.headers.get('X-OpenAI-Thread-ID');
if (!threadId) {
return null;
}
return {
threadId,

Copilot uses AI. Check for mistakes.
Comment on lines +96 to +98
const reader = response.body.getReader();
const decoder = new TextDecoder();

Copilot AI Feb 14, 2026

Copy link

Choose a reason for hiding this comment

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

response.body can be null (e.g., certain error/empty responses or unsupported streaming scenarios). Calling response.body.getReader() without a guard can throw at runtime and will fail hard rather than surfacing a handled error. Add an explicit null check and handle it (e.g., throw a descriptive error or fall back to response.text()).

Suggested change
const reader = response.body.getReader();
const decoder = new TextDecoder();
const decoder = new TextDecoder();
if (!response.body) {
// Fallback for environments or responses where streaming is not available.
const text = await response.text();
async function* fallbackTextStream(): AsyncGenerator<string> {
if (text) {
yield text;
}
}
return { headers: response.headers, stream: fallbackTextStream() };
}
const reader = response.body.getReader();

Copilot uses AI. Check for mistakes.
const { done, value } = await reader.read();
if (done) break;
yield decoder.decode(value, { stream: true });
}

Copilot AI Feb 14, 2026

Copy link

Choose a reason for hiding this comment

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

When streaming-decoding with TextDecoder and { stream: true }, the decoder may buffer an incomplete multi-byte sequence at chunk boundaries. The generator currently never flushes the decoder at end-of-stream, which can drop the final character(s). After the read loop completes, flush with a final decoder.decode() (and yield it if non-empty).

Suggested change
}
}
const finalChunk = decoder.decode();
if (finalChunk) {
yield finalChunk;
}

Copilot uses AI. Check for mistakes.
this.angulartics2.eventTrack.next({
action: 'AI panel: thread created successfully',
});
posthog.capture('AI panel: thread created successfully');

Copilot AI Feb 14, 2026

Copy link

Choose a reason for hiding this comment

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

ApiService.postStream() returns null on non-abort errors after showing a global notification. In createThread(), if response is null (the likely error path), the UI currently does not add an ai-error message to the chat chain (the catch block won’t run because nothing is thrown), so the user sees their prompt with no response/error. Handle the null result explicitly (e.g., push an ai-error message / analytics) or change AiService/ApiService to throw on request failures so this catch is actually reachable.

Suggested change
posthog.capture('AI panel: thread created successfully');
posthog.capture('AI panel: thread created successfully');
} else {
// Handle non-throwing failure case from createThread (e.g., postStream returned null)
this.messagesChain.push({
type: 'ai-error',
text: 'AI request failed. Please try again.',
});
this.angulartics2.eventTrack.next({
action: 'AI panel: thread creation returned an error',
});
posthog.capture('AI panel: thread creation returned an error');

Copilot uses AI. Check for mistakes.
gugu and others added 4 commits February 15, 2026 15:10
…n AI panel

Previously _consumeStream called markdownService.parse() and then passed the
resulting HTML into <markdown> content projection, causing double-rendering.
Now raw markdown is stored directly and <markdown [data]> re-renders the
whole content on each streaming chunk.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
@lyubov-voloshko lyubov-voloshko merged commit aec08c8 into main Feb 16, 2026
13 of 15 checks passed
@lyubov-voloshko lyubov-voloshko deleted the fix/nginx-ai-streaming-buffering branch February 16, 2026 09:53
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.

3 participants