(
url: string,
options: RequestInit,
signal?: AbortSignal,
)
| 240 | * backend rejected the request but responded with a structured body |
| 241 | */ |
| 242 | export async function* streamRequest<T = any>( |
| 243 | url: string, |
| 244 | options: RequestInit, |
| 245 | signal?: AbortSignal, |
| 246 | ): AsyncGenerator<StreamEvent<T>> { |
| 247 | const response = await fetchWithIdentity(url, { ...options, signal }); |
| 248 | |
| 249 | // Defensive: true transport-level errors (500 crash, 413 WSGI reject, etc.) |
| 250 | if (!response.ok) { |
| 251 | let apiError: ApiError; |
| 252 | try { |
| 253 | const body = await response.json(); |
| 254 | apiError = body.error ?? { |
| 255 | code: 'HTTP_ERROR', |
| 256 | message: `HTTP ${response.status}`, |
| 257 | retry: false, |
| 258 | }; |
| 259 | } catch { |
| 260 | apiError = { code: 'HTTP_ERROR', message: `HTTP ${response.status}`, retry: false }; |
| 261 | } |
| 262 | throw new ApiRequestError(apiError, response.status); |
| 263 | } |
| 264 | |
| 265 | // Validation errors: backend returns 200 + application/json instead of NDJSON |
| 266 | const contentType = response.headers.get('content-type') ?? ''; |
| 267 | if (contentType.includes('application/json')) { |
| 268 | const body = await response.json(); |
| 269 | if (body.status === 'error') { |
| 270 | const apiError: ApiError = body.error ?? { |
| 271 | code: 'MALFORMED_ERROR', |
| 272 | message: 'Malformed error response', |
| 273 | retry: false, |
| 274 | }; |
| 275 | throw new ApiRequestError(apiError, response.status); |
| 276 | } |
| 277 | throw new ApiRequestError( |
| 278 | { code: 'MALFORMED_STREAM_RESPONSE', message: 'Expected NDJSON stream response', retry: false }, |
| 279 | response.status, |
| 280 | ); |
| 281 | } |
| 282 | |
| 283 | const reader = response.body?.getReader(); |
| 284 | if (!reader) { |
| 285 | throw new ApiRequestError( |
| 286 | { code: 'MALFORMED_STREAM_RESPONSE', message: 'Missing stream body', retry: false }, |
| 287 | response.status, |
| 288 | ); |
| 289 | } |
| 290 | const decoder = new TextDecoder(); |
| 291 | let buffer = ''; |
| 292 | |
| 293 | try { |
| 294 | while (true) { |
| 295 | const { done, value } = await reader.read(); |
| 296 | if (done) break; |
| 297 | |
| 298 | buffer += decoder.decode(value, { stream: true }); |
| 299 | const lines = buffer.split('\n'); |
no test coverage detected