fix(server): sse abort behavior (#13153)

<!-- This is an auto-generated comment: release notes by coderabbit.ai
-->

## Summary by CodeRabbit

* **Bug Fixes**
* Improved handling of aborted client connections during streaming,
ensuring that session messages accurately reflect if a request was
aborted.
* Enhanced consistency and reliability across all streaming endpoints
when saving session messages after streaming.

<!-- end of auto-generated comment: release notes by coderabbit.ai -->
This commit is contained in:
DarkSky
2025-07-11 12:46:55 +08:00
committed by GitHub
parent 2dacba9011
commit b79439b01d
@@ -299,6 +299,13 @@ export class CopilotController implements BeforeApplicationShutdown {
this.ongoingStreamCount$.next(this.ongoingStreamCount$.value + 1); this.ongoingStreamCount$.next(this.ongoingStreamCount$.value + 1);
const { signal, onConnectionClosed } = getSignal(req); const { signal, onConnectionClosed } = getSignal(req);
let endBeforePromiseResolve = false;
onConnectionClosed(isAborted => {
if (isAborted) {
endBeforePromiseResolve = true;
}
});
const { messageId, reasoning, webSearch } = ChatQuerySchema.parse(query); const { messageId, reasoning, webSearch } = ChatQuerySchema.parse(query);
const source$ = from( const source$ = from(
@@ -322,21 +329,21 @@ export class CopilotController implements BeforeApplicationShutdown {
shared$.pipe( shared$.pipe(
reduce((acc, chunk) => acc + chunk, ''), reduce((acc, chunk) => acc + chunk, ''),
tap(buffer => { tap(buffer => {
onConnectionClosed(isAborted => { session.push({
session.push({ role: 'assistant',
role: 'assistant', content: endBeforePromiseResolve
content: isAborted ? '> Request aborted' : buffer, ? '> Request aborted'
createdAt: new Date(), : buffer,
}); createdAt: new Date(),
void session
.save()
.catch(err =>
this.logger.error(
'Failed to save session in sse stream',
err
)
);
}); });
void session
.save()
.catch(err =>
this.logger.error(
'Failed to save session in sse stream',
err
)
);
}), }),
ignoreElements() ignoreElements()
) )
@@ -384,6 +391,13 @@ export class CopilotController implements BeforeApplicationShutdown {
this.ongoingStreamCount$.next(this.ongoingStreamCount$.value + 1); this.ongoingStreamCount$.next(this.ongoingStreamCount$.value + 1);
const { signal, onConnectionClosed } = getSignal(req); const { signal, onConnectionClosed } = getSignal(req);
let endBeforePromiseResolve = false;
onConnectionClosed(isAborted => {
if (isAborted) {
endBeforePromiseResolve = true;
}
});
const { messageId, reasoning, webSearch } = ChatQuerySchema.parse(query); const { messageId, reasoning, webSearch } = ChatQuerySchema.parse(query);
const source$ = from( const source$ = from(
@@ -407,25 +421,25 @@ export class CopilotController implements BeforeApplicationShutdown {
shared$.pipe( shared$.pipe(
reduce((acc, chunk) => acc.concat([chunk]), [] as StreamObject[]), reduce((acc, chunk) => acc.concat([chunk]), [] as StreamObject[]),
tap(result => { tap(result => {
onConnectionClosed(isAborted => { const parser = new StreamObjectParser();
const parser = new StreamObjectParser(); const streamObjects = parser.mergeTextDelta(result);
const streamObjects = parser.mergeTextDelta(result); const content = parser.mergeContent(streamObjects);
const content = parser.mergeContent(streamObjects); session.push({
session.push({ role: 'assistant',
role: 'assistant', content: endBeforePromiseResolve
content: isAborted ? '> Request aborted' : content, ? '> Request aborted'
streamObjects: isAborted ? null : streamObjects, : content,
createdAt: new Date(), streamObjects: endBeforePromiseResolve ? null : streamObjects,
}); createdAt: new Date(),
void session
.save()
.catch(err =>
this.logger.error(
'Failed to save session in sse stream',
err
)
);
}); });
void session
.save()
.catch(err =>
this.logger.error(
'Failed to save session in sse stream',
err
)
);
}), }),
ignoreElements() ignoreElements()
) )
@@ -477,6 +491,13 @@ export class CopilotController implements BeforeApplicationShutdown {
this.ongoingStreamCount$.next(this.ongoingStreamCount$.value + 1); this.ongoingStreamCount$.next(this.ongoingStreamCount$.value + 1);
const { signal, onConnectionClosed } = getSignal(req); const { signal, onConnectionClosed } = getSignal(req);
let endBeforePromiseResolve = false;
onConnectionClosed(isAborted => {
if (isAborted) {
endBeforePromiseResolve = true;
}
});
const source$ = from( const source$ = from(
this.workflow.runGraph(params, session.model, { this.workflow.runGraph(params, session.model, {
...session.config.promptConfig, ...session.config.promptConfig,
@@ -526,21 +547,21 @@ export class CopilotController implements BeforeApplicationShutdown {
return acc; return acc;
}, ''), }, ''),
tap(content => { tap(content => {
onConnectionClosed(isAborted => { session.push({
session.push({ role: 'assistant',
role: 'assistant', content: endBeforePromiseResolve
content: isAborted ? '> Request aborted' : content, ? '> Request aborted'
createdAt: new Date(), : content,
}); createdAt: new Date(),
void session
.save()
.catch(err =>
this.logger.error(
'Failed to save session in sse stream',
err
)
);
}); });
void session
.save()
.catch(err =>
this.logger.error(
'Failed to save session in sse stream',
err
)
);
}), }),
ignoreElements() ignoreElements()
) )
@@ -604,6 +625,13 @@ export class CopilotController implements BeforeApplicationShutdown {
this.ongoingStreamCount$.next(this.ongoingStreamCount$.value + 1); this.ongoingStreamCount$.next(this.ongoingStreamCount$.value + 1);
const { signal, onConnectionClosed } = getSignal(req); const { signal, onConnectionClosed } = getSignal(req);
let endBeforePromiseResolve = false;
onConnectionClosed(isAborted => {
if (isAborted) {
endBeforePromiseResolve = true;
}
});
const source$ = from( const source$ = from(
provider.streamImages( provider.streamImages(
{ {
@@ -639,22 +667,20 @@ export class CopilotController implements BeforeApplicationShutdown {
shared$.pipe( shared$.pipe(
reduce((acc, chunk) => acc.concat([chunk]), [] as string[]), reduce((acc, chunk) => acc.concat([chunk]), [] as string[]),
tap(attachments => { tap(attachments => {
onConnectionClosed(isAborted => { session.push({
session.push({ role: 'assistant',
role: 'assistant', content: endBeforePromiseResolve ? '> Request aborted' : '',
content: isAborted ? '> Request aborted' : '', attachments: endBeforePromiseResolve ? [] : attachments,
attachments: isAborted ? [] : attachments, createdAt: new Date(),
createdAt: new Date(),
});
void session
.save()
.catch(err =>
this.logger.error(
'Failed to save session in sse stream',
err
)
);
}); });
void session
.save()
.catch(err =>
this.logger.error(
'Failed to save session in sse stream',
err
)
);
}), }),
ignoreElements() ignoreElements()
) )