Skip to content

Commit 8bab657

Browse files
committed
fix: prevent AI assistant crash on non-2xx streaming responses
Streaming AI requests keep the response body as a lazy stream, but the error handler assumed a decoded map and keyed into it directly. A non-success status from a streaming endpoint therefore crashed the handler and discarded the background job, masking the real error. Return a clean error message unless the body is a plain map with a string "message"; any other shape (a stream body, or a nested or non-string message) falls back to a generic message, and the failing HTTP status is logged. Fixes #4941
1 parent 4cc01f5 commit 8bab657

3 files changed

Lines changed: 49 additions & 2 deletions

File tree

CHANGELOG.md

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -37,6 +37,10 @@ and this project adheres to
3737

3838
### Fixed
3939

40+
- Return a clean error instead of crashing when the AI assistant's streaming
41+
endpoint responds with a non-success status.
42+
[#4941](https://github.com/OpenFn/lightning/issues/4941)
43+
4044
## [2.16.8] - 2026-07-01
4145

4246
## [2.16.8-pre] - 2026-06-18

lib/lightning/ai_assistant/ai_assistant.ex

Lines changed: 12 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1434,10 +1434,10 @@ defmodule Lightning.AiAssistant do
14341434
case error_response do
14351435
{:ok, %Tesla.Env{status: status, body: body}}
14361436
when status not in @success_status_range ->
1437-
error_message = body["message"]
1437+
error_message = error_message_from_body(body)
14381438

14391439
Logger.error(
1440-
"AI query failed for session #{session.id}: #{error_message}"
1440+
"AI query failed for session #{session.id} (status #{status}): #{error_message}"
14411441
)
14421442

14431443
{:error, error_message}
@@ -1459,6 +1459,16 @@ defmodule Lightning.AiAssistant do
14591459
end
14601460
end
14611461

1462+
# A streaming body is a lazy Stream and a JSON "message" may be a nested
1463+
# object, so only a plain-map body with a string message is safe to return.
1464+
defp error_message_from_body(body) do
1465+
message = if is_map(body) and not is_struct(body), do: body["message"]
1466+
1467+
if is_binary(message),
1468+
do: message,
1469+
else: "Oops! Something went wrong. Please try again."
1470+
end
1471+
14621472
defp build_job_message(body) do
14631473
message = body["history"] |> Enum.reverse() |> hd()
14641474
message_attrs = Map.take(message, ["role", "content"])

test/lightning/ai_assistant/ai_assistant_test.exs

Lines changed: 33 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3392,6 +3392,39 @@ defmodule Lightning.AiAssistantTest do
33923392
assert_received {:ai_assistant, :streaming_error,
33933393
%{error: "Streaming connection lost", session_id: _}}
33943394
end
3395+
3396+
test "returns a clean binary error tuple for a non-2xx response with any body shape",
3397+
%{user: user, workflow: %{jobs: [job_1 | _]}} do
3398+
session =
3399+
insert(:chat_session,
3400+
user: user,
3401+
job: job_1,
3402+
messages: [
3403+
%{
3404+
role: :user,
3405+
content: "help me",
3406+
user: user,
3407+
status: :pending,
3408+
inserted_at: DateTime.utc_now() |> DateTime.add(-1)
3409+
}
3410+
]
3411+
)
3412+
3413+
# Each shape (Stream function, %Stream{} struct, nested-message map)
3414+
# previously crashed the handler; assert it now yields a binary error.
3415+
for body <- [
3416+
Stream.unfold(:done, fn _ -> nil end),
3417+
Stream.map([], & &1),
3418+
%{"message" => %{"detail" => "invalid input"}}
3419+
] do
3420+
expect(Lightning.Tesla.Mock, :call, fn _env, _opts ->
3421+
{:ok, %Tesla.Env{status: 500, body: body}}
3422+
end)
3423+
3424+
assert {:error, message} = AiAssistant.query_stream(session, "help me")
3425+
assert is_binary(message)
3426+
end
3427+
end
33953428
end
33963429

33973430
describe "query_workflow_stream/3" do

0 commit comments

Comments
 (0)