Dify API Streaming Issue: Fetch in Vue Returns Response All at Once Instead of Streaming #11407

Closed
opened 2026-02-21 18:59:18 -05:00 by yindo · 0 comments
Owner

Originally created by @lucasmen9527 on GitHub (Mar 17, 2025).

Self Checks

  • I have searched for existing issues search for existing issues, including closed ones.
  • I confirm that I am using English to submit report (我已阅读并同意 Language Policy).
  • [FOR CHINESE USERS] 请务必使用英文提交 Issue,否则会被关闭。谢谢!:)
  • Please do not modify this template :) and fill in all the required fields.

Provide a description of requested docs changes

I deployed Dify using Docker and integrated it with a third-party API from DeepSeek.

Currently, I’m encountering an issue where:

When I send a conversation request using curl, the response is returned as an event stream (chunked transfer encoding), as expected.
However, when I send an HTTP request from the frontend (Vue) using fetch with reader or fetchEventSource, the response appears to be buffered and returned all at once instead of streaming progressively.
Interestingly, when I test the same API request on the Dify web platform (after publishing my app), I can see that the event stream is correctly printed step by step in the network request logs.
I would like to understand why this happens and how to ensure that my Vue frontend properly handles streaming responses in real-time, just like curl or the Dify platform does.

我使用的docker部署的dify 然后对接的deepseek第三方的api 我现在遇到的问题是 如果我用 curl 正常发对话请求 返回的结果是event 分块传输 但是如果我用前端 vue发送http请求 比如用fetch结合reader 或者用fetchEventSource 发送 dify的 api 的http请求 就会发现evenStream格式是请求半天一次性响应回来的 我去用dify平台的网页发布后 去看网络请求 eventStream也是逐步打印

This is curl's request
Image

This is vue's request script
`<script>
import { fetchEventSource } from "@microsoft/fetch-event-source";

export default {
data() {
return {
// 接口类型选择:dify 或 deepseek
apiType: "dify",
// API Key 分别输入
difyApiKey: "app-xxx",
deepseekApiKey: "sk-xxx

  userId: "abc-123",
  inputText: "",
  messages: [],
  isLoading: false,
  controller: null,
  // 打字机相关状态
  typingQueue: [],
  typingInterval: null,
  typingSpeed: 20, // 每个字符显示间隔(毫秒)
  currentMessageId: null,
  // 接口地址
  difyEndpoint: "/v1/chat-messages", // 根据实际情况设置
  deepseekEndpoint: "https://api.deepseek.com/chat/completions",
};

},
methods: {
async sendMessage() {
if (!this.inputText.trim()) return;

  this.isLoading = true;
  const query = this.inputText;
  this.inputText = "";

  // 清除之前的打字效果
  this.clearTyping();

  // 添加新消息
  const newMsg = {
    id: Date.now(),
    query,
    response: "",
    isTyping: true,
    isError: false,
  };
  this.messages.push(newMsg);
  this.currentMessageId = newMsg.id;

  if (this.controller) {
    this.controller.abort();
  }
  this.controller = new AbortController();

  // 根据选择的接口确定端点和请求体、API Key
  let endpoint = "";
  let requestBody = {};
  let currentApiKey = "";
  if (this.apiType === "dify") {
    endpoint = this.difyEndpoint;
    currentApiKey = this.difyApiKey;
    requestBody = {
      inputs: {},
      query,
      response_mode: "streaming",

      conversation_id: "",
      user: this.userId,
      files: [], // 可根据需要添加文件
    };
  } else if (this.apiType === "deepseek") {
    endpoint = this.deepseekEndpoint;
    currentApiKey = this.deepseekApiKey;
    // DeepSeek 官方接口示例(此处 stream 设置为 true,若不支持流式则可设置为 false)
    requestBody = {
      model: "deepseek-chat",
      messages: [
        { role: "system", content: "You are a helpful assistant." },
        { role: "user", content: query },
      ],
      stream: true,
    };
  }

  try {
    await fetchEventSource(endpoint, {
      method: "POST",
      headers: {
        Authorization: `Bearer ${currentApiKey}`,
        "Content-Type": "application/json",
      },
      body: JSON.stringify(requestBody),
      signal: this.controller.signal,
      onmessage: (event) => {
        console.log("[SSE RAW]", event); // 打印原始事件对象
        if (event.data === "[DONE]") {
          console.log("接收到结束标记 [DONE]");
          return;
        }
        try {
          const data = JSON.parse(event.data);
          console.log("原始数据块:", data);
          if (data.answer) {
            this.addToTypingQueue(data.answer);
          }
        } catch (e) {
          console.error("解析错误:", e);
        }
      },
      onerror: (err) => {
        console.error("流式请求错误:", err);
        this.appendError("服务请求失败,请检查 API Key 和网络");
        this.clearTyping();
      },
      onclose: () => {
        this.isLoading = false;
        this.finishTyping();
      },
    });
    // const response = await fetch(endpoint, {
    //   method: "POST",
    //   headers: {
    //     Authorization: `Bearer ${currentApiKey}`,
    //     "Content-Type": "application/json",
    //   },
    //   body: JSON.stringify(requestBody),
    // });
    // const reader = response.body.getReader();
    // const decoder = new TextDecoder();
    // // eslint-disable-next-line
    // while (true) {
    //   const { done, value } = await reader.read();
    //   if (done) break;
    //   const chunk = decoder.decode(value, { stream: true });
    //   console.log("流式数据块:", chunk);
    // }
  } catch (error) {
    console.error("请求失败:", error);
    this.appendError("服务请求失败,请检查 API Key 和网络");
  } finally {
    this.isLoading = false;
  }
},

// 将数据加入打字队列
addToTypingQueue(text) {
  const characters = text.split("");
  this.typingQueue.push(...characters);

  if (!this.typingInterval) {
    this.startTyping();
  }
},

// 启动打字效果
startTyping() {
  this.typingInterval = setInterval(() => {
    if (this.typingQueue.length > 0) {
      const char = this.typingQueue.shift();
      const lastMsg = this.messages.find(
        (msg) => msg.id === this.currentMessageId
      );
      if (lastMsg) {
        lastMsg.response += char;
        this.forceScroll();
      }
    } else {
      this.clearTyping();
    }
  }, this.typingSpeed);
},

// 强制滚动到底部
forceScroll() {
  this.$nextTick(() => {
    const container = this.$el.querySelector(".message-list");
    if (container) {
      container.scrollTop = container.scrollHeight + 20;
    }
  });
},

// 完成打字效果
finishTyping() {
  const lastMsg = this.messages.find(
    (msg) => msg.id === this.currentMessageId
  );
  if (lastMsg) {
    lastMsg.isTyping = false;
  }
},

// 清除打字效果
clearTyping() {
  if (this.typingInterval) {
    clearInterval(this.typingInterval);
    this.typingInterval = null;
  }
  this.typingQueue = [];
  this.finishTyping();
},

appendError(error) {
  this.clearTyping();
  this.messages.push({
    id: Date.now(),
    query: "系统提示",
    response: error,
    isError: true,
    isTyping: false,
  });
},

},
beforeUnmount() {
this.clearTyping();
if (this.controller) {
this.controller.abort();
}
},
};
</script>`
This is the result of the console print, and the console prints a response that is half a day and returned at one time
Image

Originally created by @lucasmen9527 on GitHub (Mar 17, 2025). ### Self Checks - [x] I have searched for existing issues [search for existing issues](https://github.com/langgenius/dify/issues), including closed ones. - [x] I confirm that I am using English to submit report (我已阅读并同意 [Language Policy](https://github.com/langgenius/dify/issues/1542)). - [x] [FOR CHINESE USERS] 请务必使用英文提交 Issue,否则会被关闭。谢谢!:) - [x] Please do not modify this template :) and fill in all the required fields. ### Provide a description of requested docs changes I deployed Dify using Docker and integrated it with a third-party API from DeepSeek. Currently, I’m encountering an issue where: When I send a conversation request using curl, the response is returned as an event stream (chunked transfer encoding), as expected. However, when I send an HTTP request from the frontend (Vue) using fetch with reader or fetchEventSource, the response appears to be buffered and returned all at once instead of streaming progressively. Interestingly, when I test the same API request on the Dify web platform (after publishing my app), I can see that the event stream is correctly printed step by step in the network request logs. I would like to understand why this happens and how to ensure that my Vue frontend properly handles streaming responses in real-time, just like curl or the Dify platform does. 我使用的docker部署的dify 然后对接的deepseek第三方的api 我现在遇到的问题是 如果我用 curl 正常发对话请求 返回的结果是event 分块传输 但是如果我用前端 vue发送http请求 比如用fetch结合reader 或者用fetchEventSource 发送 dify的 api 的http请求 就会发现evenStream格式是请求半天一次性响应回来的 我去用dify平台的网页发布后 去看网络请求 eventStream也是逐步打印 This is curl's request <img width="1046" alt="Image" src="https://github.com/user-attachments/assets/f4d22e22-76df-4e51-bd75-7e9d75b93c1f" /> This is vue's request script `<script> import { fetchEventSource } from "@microsoft/fetch-event-source"; export default { data() { return { // 接口类型选择:dify 或 deepseek apiType: "dify", // API Key 分别输入 difyApiKey: "app-xxx", deepseekApiKey: "sk-xxx userId: "abc-123", inputText: "", messages: [], isLoading: false, controller: null, // 打字机相关状态 typingQueue: [], typingInterval: null, typingSpeed: 20, // 每个字符显示间隔(毫秒) currentMessageId: null, // 接口地址 difyEndpoint: "/v1/chat-messages", // 根据实际情况设置 deepseekEndpoint: "https://api.deepseek.com/chat/completions", }; }, methods: { async sendMessage() { if (!this.inputText.trim()) return; this.isLoading = true; const query = this.inputText; this.inputText = ""; // 清除之前的打字效果 this.clearTyping(); // 添加新消息 const newMsg = { id: Date.now(), query, response: "", isTyping: true, isError: false, }; this.messages.push(newMsg); this.currentMessageId = newMsg.id; if (this.controller) { this.controller.abort(); } this.controller = new AbortController(); // 根据选择的接口确定端点和请求体、API Key let endpoint = ""; let requestBody = {}; let currentApiKey = ""; if (this.apiType === "dify") { endpoint = this.difyEndpoint; currentApiKey = this.difyApiKey; requestBody = { inputs: {}, query, response_mode: "streaming", conversation_id: "", user: this.userId, files: [], // 可根据需要添加文件 }; } else if (this.apiType === "deepseek") { endpoint = this.deepseekEndpoint; currentApiKey = this.deepseekApiKey; // DeepSeek 官方接口示例(此处 stream 设置为 true,若不支持流式则可设置为 false) requestBody = { model: "deepseek-chat", messages: [ { role: "system", content: "You are a helpful assistant." }, { role: "user", content: query }, ], stream: true, }; } try { await fetchEventSource(endpoint, { method: "POST", headers: { Authorization: `Bearer ${currentApiKey}`, "Content-Type": "application/json", }, body: JSON.stringify(requestBody), signal: this.controller.signal, onmessage: (event) => { console.log("[SSE RAW]", event); // 打印原始事件对象 if (event.data === "[DONE]") { console.log("接收到结束标记 [DONE]"); return; } try { const data = JSON.parse(event.data); console.log("原始数据块:", data); if (data.answer) { this.addToTypingQueue(data.answer); } } catch (e) { console.error("解析错误:", e); } }, onerror: (err) => { console.error("流式请求错误:", err); this.appendError("服务请求失败,请检查 API Key 和网络"); this.clearTyping(); }, onclose: () => { this.isLoading = false; this.finishTyping(); }, }); // const response = await fetch(endpoint, { // method: "POST", // headers: { // Authorization: `Bearer ${currentApiKey}`, // "Content-Type": "application/json", // }, // body: JSON.stringify(requestBody), // }); // const reader = response.body.getReader(); // const decoder = new TextDecoder(); // // eslint-disable-next-line // while (true) { // const { done, value } = await reader.read(); // if (done) break; // const chunk = decoder.decode(value, { stream: true }); // console.log("流式数据块:", chunk); // } } catch (error) { console.error("请求失败:", error); this.appendError("服务请求失败,请检查 API Key 和网络"); } finally { this.isLoading = false; } }, // 将数据加入打字队列 addToTypingQueue(text) { const characters = text.split(""); this.typingQueue.push(...characters); if (!this.typingInterval) { this.startTyping(); } }, // 启动打字效果 startTyping() { this.typingInterval = setInterval(() => { if (this.typingQueue.length > 0) { const char = this.typingQueue.shift(); const lastMsg = this.messages.find( (msg) => msg.id === this.currentMessageId ); if (lastMsg) { lastMsg.response += char; this.forceScroll(); } } else { this.clearTyping(); } }, this.typingSpeed); }, // 强制滚动到底部 forceScroll() { this.$nextTick(() => { const container = this.$el.querySelector(".message-list"); if (container) { container.scrollTop = container.scrollHeight + 20; } }); }, // 完成打字效果 finishTyping() { const lastMsg = this.messages.find( (msg) => msg.id === this.currentMessageId ); if (lastMsg) { lastMsg.isTyping = false; } }, // 清除打字效果 clearTyping() { if (this.typingInterval) { clearInterval(this.typingInterval); this.typingInterval = null; } this.typingQueue = []; this.finishTyping(); }, appendError(error) { this.clearTyping(); this.messages.push({ id: Date.now(), query: "系统提示", response: error, isError: true, isTyping: false, }); }, }, beforeUnmount() { this.clearTyping(); if (this.controller) { this.controller.abort(); } }, }; </script>` This is the result of the console print, and the console prints a response that is half a day and returned at one time <img width="1460" alt="Image" src="https://github.com/user-attachments/assets/191820b2-a548-4e69-95a5-f6174947b0f9" />
yindo added the 🐞 bug label 2026-02-21 18:59:18 -05:00
yindo closed this issue 2026-02-21 18:59:18 -05:00
Sign in to join this conversation.
1 Participants
Notifications
Due Date
No due date set.
Dependencies

No dependencies set.

Reference: langgenius/dify#11407