This is an automated email from the ASF dual-hosted git repository.

AlinsRan pushed a commit to branch master
in repository https://gitbox.apache.org/repos/asf/apisix.git


The following commit(s) were added to refs/heads/master by this push:
     new 5abb66e39 fix(ai-proxy): return 502 when a streaming upstream produces 
no output (#13870)
5abb66e39 is described below

commit 5abb66e39db992f92cfa8fa32c7c495211f36f42
Author: AlinsRan <[email protected]>
AuthorDate: Mon Aug 24 16:34:19 2026 +0800

    fix(ai-proxy): return 502 when a streaming upstream produces no output 
(#13870)
---
 apisix/plugins/ai-providers/base.lua |  10 ++--
 t/plugin/ai-proxy-empty-stream.t     | 109 +++++++++++++++++++++++++++++++++++
 2 files changed, 115 insertions(+), 4 deletions(-)

diff --git a/apisix/plugins/ai-providers/base.lua 
b/apisix/plugins/ai-providers/base.lua
index 61b1c9529..c8a5d6a71 100644
--- a/apisix/plugins/ai-providers/base.lua
+++ b/apisix/plugins/ai-providers/base.lua
@@ -576,13 +576,15 @@ function _M.parse_streaming_response(self, ctx, res, 
target_proto, converter, co
             res._upstream_bytes = bytes_read
             ctx.var.apisix_upstream_response_time = math.floor(
                 (ngx_now() - ctx.llm_request_start_time) * 1000)
-            if converter and not output_sent then
+            if not output_sent then
                 if flush_thread then
                     ngx.thread.kill(flush_thread)
                 end
-                local msg = "streaming response completed without producing "
-                            .. "any output; the upstream likely returned a "
-                            .. "different stream format than the converter 
expects"
+                local msg = converter
+                    and "streaming response completed without producing "
+                        .. "any output; the upstream likely returned a "
+                        .. "different stream format than the converter expects"
+                    or "upstream returned an empty stream"
                 core.log.error(msg)
                 return 502, msg
             end
diff --git a/t/plugin/ai-proxy-empty-stream.t b/t/plugin/ai-proxy-empty-stream.t
new file mode 100644
index 000000000..bfec410c7
--- /dev/null
+++ b/t/plugin/ai-proxy-empty-stream.t
@@ -0,0 +1,109 @@
+#
+# Licensed to the Apache Software Foundation (ASF) under one or more
+# contributor license agreements.  See the NOTICE file distributed with
+# this work for additional information regarding copyright ownership.
+# The ASF licenses this file to You under the Apache License, Version 2.0
+# (the "License"); you may not use this file except in compliance with
+# the License.  You may obtain a copy of the License at
+#
+#     http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+#
+
+use t::APISIX 'no_plan';
+
+log_level("info");
+repeat_each(1);
+no_long_string();
+no_root_location();
+
+
+add_block_preprocessor(sub {
+    my ($block) = @_;
+
+    if (!defined $block->request) {
+        $block->set_value("request", "GET /t");
+    }
+
+    # Mock upstream reproducing the customer incident: the LLM answers with a
+    # 200 and an SSE content type, then closes the connection without ever
+    # writing a single body byte. No converter is involved (OpenAI-format
+    # client against an OpenAI-compatible provider).
+    my $http_config = $block->http_config // <<_EOC_;
+        server {
+            server_name empty_sse;
+            listen 7751;
+
+            location /v1/chat/completions {
+                content_by_lua_block {
+                    ngx.status = 200
+                    ngx.header["Content-Type"] = "text/event-stream"
+                    -- headers only, zero body bytes, then EOF
+                    ngx.eof()
+                }
+            }
+        }
+_EOC_
+
+    $block->set_value("http_config", $http_config);
+});
+
+run_tests();
+
+__DATA__
+
+=== TEST 1: set route with a streaming ai-proxy against the empty-SSE upstream
+--- config
+    location /t {
+        content_by_lua_block {
+            local t = require("lib.test_admin").test
+            local code, body = t('/apisix/admin/routes/1',
+                 ngx.HTTP_PUT,
+                 [[{
+                    "uri": "/anything",
+                    "plugins": {
+                        "ai-proxy": {
+                            "provider": "openai",
+                            "auth": {
+                                "header": {
+                                    "Authorization": "Bearer token"
+                                }
+                            },
+                            "options": {
+                                "model": "gpt-4",
+                                "stream": true
+                            },
+                            "override": {
+                                "endpoint": "http://localhost:7751";
+                            },
+                            "ssl_verify": false
+                        }
+                    }
+                }]]
+            )
+
+            if code >= 300 then
+                ngx.status = code
+            end
+            ngx.say(body)
+        }
+    }
+--- response_body
+passed
+
+
+
+=== TEST 2: upstream returns an empty SSE stream
+--- request
+POST /anything
+{"messages": [{"role": "user", "content": "hi"}]}
+--- more_headers
+Content-Type: application/json
+--- error_code: 502
+--- no_error_log
+attempt to index local 'up_conf'

Reply via email to