fix: stream GET /chats/all to prevent OOM on large chat histories (#24461)
Convert the /chats/all endpoint from loading all user chats into memory at once to a streaming NDJSON response that fetches chats in batches of 100. This prevents Out-of-Memory crashes for users with large chat histories. Backend: Added async generator that paginates through chats with short-lived DB sessions per batch (critical for SQLite lock release). Frontend: Updated getAllChats to consume the NDJSON stream via ReadableStream reader, accumulating results for the export file. Ref: open-webui#22206
This commit is contained in:
@@ -676,11 +676,46 @@ async def get_user_pinned_chats(user=Depends(get_verified_user), db: AsyncSessio
|
||||
# GetChats
|
||||
############################
|
||||
|
||||
CHAT_EXPORT_BATCH_SIZE = 100
|
||||
|
||||
@router.get('/all', response_model=list[ChatResponse])
|
||||
async def get_user_chats(user=Depends(get_verified_user), db: AsyncSession = Depends(get_async_session)):
|
||||
result = await Chats.get_chats_by_user_id(user.id, db=db)
|
||||
return [ChatResponse(**chat.model_dump()) for chat in result.items]
|
||||
|
||||
async def generate_chat_export_ndjson(user_id: str):
|
||||
"""
|
||||
Async generator that streams all user chats as NDJSON (one JSON object per line).
|
||||
|
||||
Uses short-lived DB sessions per batch to avoid holding locks for the
|
||||
entire duration, which is critical for SQLite environments.
|
||||
"""
|
||||
skip = 0
|
||||
|
||||
while True:
|
||||
result = await Chats.get_chats_by_user_id(
|
||||
user_id,
|
||||
skip=skip,
|
||||
limit=CHAT_EXPORT_BATCH_SIZE,
|
||||
db=None,
|
||||
)
|
||||
if not result.items:
|
||||
break
|
||||
|
||||
for chat in result.items:
|
||||
try:
|
||||
yield ChatResponse(**chat.model_dump()).model_dump_json() + '\n'
|
||||
except Exception as e:
|
||||
log.exception(f'Error serializing chat {chat.id}: {e}')
|
||||
|
||||
if len(result.items) < CHAT_EXPORT_BATCH_SIZE:
|
||||
break
|
||||
|
||||
skip += CHAT_EXPORT_BATCH_SIZE
|
||||
|
||||
|
||||
@router.get('/all')
|
||||
async def get_user_chats(user=Depends(get_verified_user)):
|
||||
return StreamingResponse(
|
||||
generate_chat_export_ndjson(user.id),
|
||||
media_type='application/x-ndjson',
|
||||
)
|
||||
|
||||
|
||||
############################
|
||||
|
||||
+39
-20
@@ -301,34 +301,53 @@ export const getSharedChatList = async (token: string = '', page: number = 1, fi
|
||||
};
|
||||
|
||||
export const getAllChats = async (token: string) => {
|
||||
let error = null;
|
||||
|
||||
const res = await fetch(`${WEBUI_API_BASE_URL}/chats/all`, {
|
||||
method: 'GET',
|
||||
headers: {
|
||||
Accept: 'application/json',
|
||||
'Content-Type': 'application/json',
|
||||
Accept: 'application/x-ndjson',
|
||||
...(token && { authorization: `Bearer ${token}` })
|
||||
}
|
||||
})
|
||||
.then(async (res) => {
|
||||
if (!res.ok) throw await res.json();
|
||||
return res.json();
|
||||
})
|
||||
.then((json) => {
|
||||
return json;
|
||||
})
|
||||
.catch((err) => {
|
||||
error = err;
|
||||
console.error(err);
|
||||
return null;
|
||||
});
|
||||
});
|
||||
|
||||
if (error) {
|
||||
throw error;
|
||||
if (!res.ok) {
|
||||
const err = await res.json();
|
||||
console.error(err);
|
||||
throw err;
|
||||
}
|
||||
|
||||
return res;
|
||||
const reader = res.body?.getReader();
|
||||
if (!reader) {
|
||||
throw new Error('Response body is not readable');
|
||||
}
|
||||
|
||||
const decoder = new TextDecoder();
|
||||
const chats: object[] = [];
|
||||
let buffer = '';
|
||||
|
||||
while (true) {
|
||||
const { done, value } = await reader.read();
|
||||
if (done) break;
|
||||
|
||||
buffer += decoder.decode(value, { stream: true });
|
||||
const lines = buffer.split('\n');
|
||||
// Keep the last potentially incomplete line in the buffer
|
||||
buffer = lines.pop() ?? '';
|
||||
|
||||
for (const line of lines) {
|
||||
const trimmed = line.trim();
|
||||
if (trimmed) {
|
||||
chats.push(JSON.parse(trimmed));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Process any remaining data in the buffer
|
||||
const remaining = buffer.trim();
|
||||
if (remaining) {
|
||||
chats.push(JSON.parse(remaining));
|
||||
}
|
||||
|
||||
return chats;
|
||||
};
|
||||
|
||||
export const getChatListBySearchText = async (token: string, text: string, page: number = 1) => {
|
||||
|
||||
Reference in New Issue
Block a user