@@ -10,7 +10,7 @@ vi.mock("./auth-headers", () => ({
1010 withBackendAuth : withBackendAuthMock ,
1111} ) )
1212
13- import { apiBaseUrl , proxyJson , proxyText } from "./backend-proxy"
13+ import { apiBaseUrl , proxyJson , proxyStream , proxyText } from "./backend-proxy"
1414
1515describe ( "apiBaseUrl" , ( ) => {
1616 it ( "uses the shared backend base URL helper" , ( ) => {
@@ -172,4 +172,84 @@ describe("backend proxy helpers", () => {
172172 )
173173 expect ( res . status ) . toBe ( 200 )
174174 } )
175+
176+ it ( "passes through SSE responses without buffering" , async ( ) => {
177+ withBackendAuthMock . mockResolvedValue ( {
178+ "Content-Type" : "application/json" ,
179+ authorization : "Bearer stream-token" ,
180+ } )
181+
182+ const fetchMock = vi . fn ( async ( ) =>
183+ new Response ( "data: ping\n\n" , {
184+ status : 200 ,
185+ headers : { "Content-Type" : "text/event-stream" } ,
186+ } ) ,
187+ )
188+ global . fetch = fetchMock as typeof fetch
189+
190+ const req = new Request ( "https://localhost/api/studio/chat" , {
191+ method : "POST" ,
192+ headers : { "Content-Type" : "application/json" } ,
193+ body : JSON . stringify ( { prompt : "hello" } ) ,
194+ } )
195+ const res = await proxyStream ( req , "https://backend/api/studio/chat" , "POST" , {
196+ auth : true ,
197+ responseContentType : "text/event-stream" ,
198+ } )
199+
200+ expect ( withBackendAuthMock ) . toHaveBeenCalledTimes ( 1 )
201+ const calls = fetchMock . mock . calls as unknown [ ] [ ]
202+ const init = calls [ 0 ] ?. [ 1 ] as
203+ | ( RequestInit & { dispatcher ?: unknown } )
204+ | undefined
205+ expect ( init ) . toBeDefined ( )
206+ expect ( init ) . toMatchObject ( {
207+ method : "POST" ,
208+ headers : {
209+ "Content-Type" : "application/json" ,
210+ authorization : "Bearer stream-token" ,
211+ } ,
212+ body : JSON . stringify ( { prompt : "hello" } ) ,
213+ } )
214+ expect ( init ?. signal ) . toBeUndefined ( )
215+ expect ( init ?. dispatcher ) . toBeDefined ( )
216+ expect ( res . headers . get ( "content-type" ) ) . toContain ( "text/event-stream" )
217+ expect ( await res . text ( ) ) . toContain ( "data: ping" )
218+ } )
219+
220+ it ( "returns JSON when a stream route falls back to a non-stream response" , async ( ) => {
221+ withBackendAuthMock . mockResolvedValue ( {
222+ Accept : "text/event-stream, application/json" ,
223+ "Content-Type" : "application/json" ,
224+ authorization : "Bearer stream-token" ,
225+ } )
226+
227+ const fetchMock = vi . fn ( async ( ) =>
228+ new Response ( JSON . stringify ( { done : true } ) , {
229+ status : 200 ,
230+ headers : { "Content-Type" : "application/json" } ,
231+ } ) ,
232+ )
233+ global . fetch = fetchMock as typeof fetch
234+
235+ const req = new Request ( "https://localhost/api/research/paperscool/daily" , {
236+ method : "POST" ,
237+ headers : { "Content-Type" : "application/json" } ,
238+ body : JSON . stringify ( { query : "llm" } ) ,
239+ } )
240+ const res = await proxyStream (
241+ req ,
242+ "https://backend/api/research/paperscool/daily" ,
243+ "POST" ,
244+ {
245+ accept : "text/event-stream, application/json" ,
246+ auth : true ,
247+ passthroughNonStreamResponse : true ,
248+ responseContentType : "text/event-stream" ,
249+ } ,
250+ )
251+
252+ expect ( res . headers . get ( "content-type" ) ) . toContain ( "application/json" )
253+ await expect ( res . json ( ) ) . resolves . toEqual ( { done : true } )
254+ } )
175255} )
0 commit comments