[GH-ISSUE #22381] Pipe emit_message content not persisted to chat storage in v0.8.8 #90450

Closed
opened 2026-05-15 15:42:24 -05:00 by GiteaMirror · 2 comments
Owner

Originally created by @JasmineBlake-Author on GitHub (Mar 8, 2026).
Original GitHub issue: https://github.com/open-webui/open-webui/issues/22381

Bug Report: Pipe emit_message content not persisted to chat storage in v0.8.8

Environment

  • Open WebUI Version: v0.8.8
  • Deployment: Docker on Ubuntu 24.04
  • Database: PostgreSQL
  • Browser: Chrome 133, Firefox (both tested)

Bug Description

Custom pipe functions using emit_message() execute correctly on the backend but the emitted content is not persisted to the chat message storage. The assistant message in the database has empty content (0 bytes) despite successful emit calls.

Reproduction Steps

  1. Create a pipe function that uses emit_message() to stream content:
async def emit_message(self, message: str):
    await self.__current_event_emitter__(
        {"type": "message", "data": {"content": message}}
    )
  1. Execute the pipe via chat

  2. Observe:

    • Backend logs show emit_message being called successfully
    • __current_event_emitter__ is not None (type is <class 'function'>)
    • Event emitter returns without error
    • Chat completes normally (POST /api/chat/completed returns 200)
  3. Check database:

SELECT chat->'history'->'messages'->'<message_id>'->>'content'
FROM chat WHERE id = '<chat_id>';
-- Returns empty string despite 277KB of content being emitted

Expected Behavior

Content passed to emit_message() should be:

  1. Streamed to the browser in real-time
  2. Persisted to the assistant message content in the database

Actual Behavior

  • Content is NOT streamed to browser (blank screen during execution)
  • Content is NOT persisted to database (assistant message content is empty)
  • Chat metadata shows activity but no actual content

Evidence from Logs

2026-03-07 23:37:04,375 - Doc Review/2R - INFO - SESSION COMPLETE signal detected
2026-03-07 23:37:04.452 - POST /api/chat/completed HTTP/1.1" 200

The pipe executed 6 rounds of a multi-model conversation, all emits succeeded, but nothing was saved.

Database Evidence

-- Chat has 277KB of JSON but assistant content is empty
SELECT length(chat::text) FROM chat WHERE id = 'b7ad9209-2f6d-46bf-be64-7b47e4ac189e';
-- Returns: 277916

SELECT length((chat->'history'->'messages'->'9d24790e-c66d-4ba7-9269-75a73c3765a6'->>'content')::text) FROM chat;
-- Returns: 0
  • #19548 - Custom Pipe Responses Only Appear After Page Reload (November 2025)
  • #11750 - emit_message error when message doesn't exist
  • #20196 - AsyncGenerator pipes hang in UI

Workaround Attempted

  • Tried type: "replace" instead of type: "message" - no change
  • Tried accumulating content before emit - no change
  • Tried different browsers (Chrome, Firefox, Chrome incognito) - no change

Impact

This bug makes custom pipe functions unusable for any workflow that needs to persist conversation content. The pipe executes correctly but produces no visible or saved output.

Additional Context

  • This appears to be a regression - similar pipe configurations worked in earlier versions
  • The emitter function is valid and returns without error
  • Regular (non-pipe) model streaming works correctly in the same environment
Originally created by @JasmineBlake-Author on GitHub (Mar 8, 2026). Original GitHub issue: https://github.com/open-webui/open-webui/issues/22381 # Bug Report: Pipe emit_message content not persisted to chat storage in v0.8.8 ## Environment - **Open WebUI Version:** v0.8.8 - **Deployment:** Docker on Ubuntu 24.04 - **Database:** PostgreSQL - **Browser:** Chrome 133, Firefox (both tested) ## Bug Description Custom pipe functions using `emit_message()` execute correctly on the backend but the emitted content is not persisted to the chat message storage. The assistant message in the database has empty content (0 bytes) despite successful emit calls. ## Reproduction Steps 1. Create a pipe function that uses `emit_message()` to stream content: ```python async def emit_message(self, message: str): await self.__current_event_emitter__( {"type": "message", "data": {"content": message}} ) ``` 2. Execute the pipe via chat 3. Observe: - Backend logs show `emit_message` being called successfully - `__current_event_emitter__` is not None (type is `<class 'function'>`) - Event emitter returns without error - Chat completes normally (POST /api/chat/completed returns 200) 4. Check database: ```sql SELECT chat->'history'->'messages'->'<message_id>'->>'content' FROM chat WHERE id = '<chat_id>'; -- Returns empty string despite 277KB of content being emitted ``` ## Expected Behavior Content passed to `emit_message()` should be: 1. Streamed to the browser in real-time 2. Persisted to the assistant message content in the database ## Actual Behavior - Content is NOT streamed to browser (blank screen during execution) - Content is NOT persisted to database (assistant message content is empty) - Chat metadata shows activity but no actual content ## Evidence from Logs ``` 2026-03-07 23:37:04,375 - Doc Review/2R - INFO - SESSION COMPLETE signal detected 2026-03-07 23:37:04.452 - POST /api/chat/completed HTTP/1.1" 200 ``` The pipe executed 6 rounds of a multi-model conversation, all emits succeeded, but nothing was saved. ## Database Evidence ```sql -- Chat has 277KB of JSON but assistant content is empty SELECT length(chat::text) FROM chat WHERE id = 'b7ad9209-2f6d-46bf-be64-7b47e4ac189e'; -- Returns: 277916 SELECT length((chat->'history'->'messages'->'9d24790e-c66d-4ba7-9269-75a73c3765a6'->>'content')::text) FROM chat; -- Returns: 0 ``` ## Related Issues - #19548 - Custom Pipe Responses Only Appear After Page Reload (November 2025) - #11750 - emit_message error when message doesn't exist - #20196 - AsyncGenerator pipes hang in UI ## Workaround Attempted - Tried `type: "replace"` instead of `type: "message"` - no change - Tried accumulating content before emit - no change - Tried different browsers (Chrome, Firefox, Chrome incognito) - no change ## Impact This bug makes custom pipe functions unusable for any workflow that needs to persist conversation content. The pipe executes correctly but produces no visible or saved output. ## Additional Context - This appears to be a regression - similar pipe configurations worked in earlier versions - The emitter function is valid and returns without error - Regular (non-pipe) model streaming works correctly in the same environment
Author
Owner

@pr-validator-bot commented on GitHub (Mar 8, 2026):

⚠️ Missing Issue Title Prefix

@JasmineBlake-Author, your issue title is missing a prefix (e.g., bug:, feat:, docs:).

Please update your issue title to include one of the following prefixes:

  • bug: Bug report or error you've encountered
  • feat: Feature request or enhancement suggestion
  • docs: Documentation issue or improvement request
  • question: Question about usage or functionality
  • help: Request for help or support

Example: bug: Login fails when using special characters in password

<!-- gh-comment-id:4017789010 --> @pr-validator-bot commented on GitHub (Mar 8, 2026): # ⚠️ Missing Issue Title Prefix @JasmineBlake-Author, your issue title is missing a prefix (e.g., `bug:`, `feat:`, `docs:`). Please update your issue title to include one of the following prefixes: - **bug**: Bug report or error you've encountered - **feat**: Feature request or enhancement suggestion - **docs**: Documentation issue or improvement request - **question**: Question about usage or functionality - **help**: Request for help or support Example: `bug: Login fails when using special characters in password`
Author
Owner

@Classic298 commented on GitHub (Mar 8, 2026):

Thank you for the detailed report and reproduction steps. This is a known architectural limitation with how pipes interact with the event persistence system, not a regression specific to v0.8.8.

Root Cause

The backend event emitter does persist type: "message" events to the database — this part works correctly. However, after your pipe's pipe() method finishes executing, the frontend performs a full chat history save (via chatCompletedHandler), which writes the entire local message state back to the database.

The problem is:

  1. Your pipe emits content via type: "message" events → backend writes it to the DB
  2. Your pipe returns None (no explicit return value) → the HTTP response carries no content
  3. The frontend's local copy of the assistant message still has content: "" (or whatever accumulated via Socket.IO — which depends on timing)
  4. chatCompletedHandler fires → saves the full local history to the DB → overwrites the event-emitter-written content with the empty local state

This explains exactly what you see: 277KB of chat JSON (the overall chat structure is saved), but the assistant message's content field is empty (overwritten by the empty local state).

Workaround

The fix is to return or yield your content from the pipe() method instead of relying solely on emit_message:

# ❌ Fragile — content can be overwritten on save
async def pipe(self, body: dict, __event_emitter__=None):
    await __event_emitter__({"type": "message", "data": {"content": "Hello!"}})
    # Returns None — frontend overwrites with empty content

# ✅ Works reliably — return content directly
async def pipe(self, body: dict, __event_emitter__=None):
    await __event_emitter__({"type": "status", "data": {"description": "Working...", "done": False}})
    result = do_work()
    await __event_emitter__({"type": "status", "data": {"description": "Done", "done": True}})
    return result

# ✅ Also works — yield for streaming
async def pipe(self, body: dict, __event_emitter__=None):
    await __event_emitter__({"type": "status", "data": {"description": "Streaming...", "done": False}})
    for chunk in generate_chunks():
        yield chunk
    await __event_emitter__({"type": "status", "data": {"description": "Done", "done": True}})

Events like status, source, citation, files, embeds, and notification are safe to use alongside return/yield — they update separate fields that aren't affected by the content save.

Documentation Update

We've updated the Events documentation to explicitly document this behavior.
The changes will go live soon.

The key takeaway: for Pipes, always deliver message content via return/yield. Use events for supplementary data (status, sources, files, notifications), not as the sole content delivery mechanism.

<!-- gh-comment-id:4017833434 --> @Classic298 commented on GitHub (Mar 8, 2026): Thank you for the detailed report and reproduction steps. This is a known architectural limitation with how pipes interact with the event persistence system, not a regression specific to v0.8.8. ## Root Cause The backend event emitter **does** persist `type: "message"` events to the database — this part works correctly. However, after your pipe's `pipe()` method finishes executing, the frontend performs a full chat history save (via `chatCompletedHandler`), which writes the entire local message state back to the database. The problem is: 1. Your pipe emits content via `type: "message"` events → backend writes it to the DB ✅ 2. Your pipe returns `None` (no explicit return value) → the HTTP response carries no content 3. The frontend's local copy of the assistant message still has `content: ""` (or whatever accumulated via Socket.IO — which depends on timing) 4. `chatCompletedHandler` fires → saves the full local `history` to the DB → **overwrites** the event-emitter-written content with the empty local state This explains exactly what you see: 277KB of chat JSON (the overall chat structure is saved), but the assistant message's `content` field is empty (overwritten by the empty local state). ## Workaround The fix is to **return or yield your content** from the `pipe()` method instead of relying solely on `emit_message`: ```python # ❌ Fragile — content can be overwritten on save async def pipe(self, body: dict, __event_emitter__=None): await __event_emitter__({"type": "message", "data": {"content": "Hello!"}}) # Returns None — frontend overwrites with empty content # ✅ Works reliably — return content directly async def pipe(self, body: dict, __event_emitter__=None): await __event_emitter__({"type": "status", "data": {"description": "Working...", "done": False}}) result = do_work() await __event_emitter__({"type": "status", "data": {"description": "Done", "done": True}}) return result # ✅ Also works — yield for streaming async def pipe(self, body: dict, __event_emitter__=None): await __event_emitter__({"type": "status", "data": {"description": "Streaming...", "done": False}}) for chunk in generate_chunks(): yield chunk await __event_emitter__({"type": "status", "data": {"description": "Done", "done": True}}) ``` Events like `status`, `source`, `citation`, `files`, `embeds`, and `notification` are safe to use alongside return/yield — they update separate fields that aren't affected by the content save. ## Documentation Update We've updated the Events documentation to explicitly document this behavior. The changes will go live soon. The key takeaway: **for Pipes, always deliver message content via return/yield. Use events for supplementary data (status, sources, files, notifications), not as the sole content delivery mechanism.**
Sign in to join this conversation.
1 Participants
Notifications
Due Date
No due date set.
Dependencies

No dependencies set.

Reference: github-starred/open-webui#90450