---
title: "useApiStream"
description: "SSE streaming composable that reuses endpoints, auth and business-code validation, yielding chunks one by one with abort support."
canonical_url: "https://nuxt.mhaibaraai.cn/en/docs/composables/use-api-stream"
---
# useApiStream

> SSE streaming composable that reuses endpoints, auth and business-code validation, yielding chunks one by one with abort support.

## Usage

`useApiStream` is the reactive shell around [`$api.stream()`](#apistream). It returns a lazy async generator: the request only starts once you begin iterating. Line buffering, multi-line `data:` concatenation, comment lines and `\r\n` handling are delegated to `eventsource-parser`; events that fail to parse are skipped silently — a heartbeat or handshake line should never break the whole stream.

```vue
<script setup lang="ts">
interface ChatChunk {
  content: string
  done: boolean
}

const text = ref('')
const { status, error, stream, abort } = useApiStream<ChatChunk>()

async function start() {
  text.value = ''

  for await (const chunk of stream('/chat', { method: 'POST', body: { message: 'Hello' } })) {
    text.value += chunk.content
  }
}
</script>

<template>
  <UButton :loading="status === 'streaming'" @click="start">Start</UButton>
  <UButton v-if="status === 'streaming'" @click="abort">Abort</UButton>
  <p>{{ text }}</p>
</template>
```

Starting a new stream aborts the previous one, and so does the component scope being disposed.

## How Accept and responseType are handled

> [!WARNING]
> 
> Both defaults were settled the hard way. Check your gateway's behaviour before changing them.

**Accept defaults to the wildcard — neither application/json nor text/event-stream.** ofetch automatically adds `Accept: application/json` whenever the body is JSON-serializable, which is simply the wrong claim for a request you intend to read as a stream: a backend negotiating on `Accept` may switch to its JSON renderer because of it. Sending `Accept: text/event-stream` instead gets the whole request rejected by those same backends (DRF, for instance) — verified against one that answers "cannot satisfy the Accept HTTP header" before any business logic runs. The wildcard avoids both. Gateways that need a specific value can pass one, and an explicit header always wins:

```ts
stream('/chat', { headers: { Accept: 'text/event-stream' } })
```

**No forced responseType: 'stream'**: detection is left to ofetch's content-type handling. A real stream (`text/event-stream`) is read as a stream, while an error response (`application/json`) is still parsed as JSON so the interceptor's business-code validation can surface the backend message. Some gateways return errors as HTTP 200 with a `{ code: 500 }` envelope — forcing a stream would turn that into a stream that never yields anything. Pass it explicitly when a gateway mislabels the content type:

```ts
stream('/chat', { responseType: 'stream' })
```

A readable error is thrown when the response body is not a stream, so nothing spins silently.

## Errors and aborts

- Deliberate aborts (`abort()`, starting a new stream, scope disposal) are not errors: `status` becomes `'aborted'`, `error` stays `null` and iteration ends silently
- Any other error is written to `error`, sets `status` to `'error'`, calls `onError` and is then **re-thrown**, so a `try/catch` around `for await` can handle it
- Business errors (a `code` outside `successCodes`) are thrown by the interceptor before the stream starts, with the toast and the `movk:api:error` hook firing as usual

```ts
try {
  for await (const chunk of stream('/chat', { onError: e => console.error(e) })) {
    // ...
  }
}
catch (error) {
  // business error, HTTP error or broken stream
}
```

## $api.stream()

Outside components, or when reactive state is not needed, call `$api.stream()` directly:

```ts
const { $api } = useNuxtApp()

for await (const chunk of await $api.stream<ChatChunk>('/chat', { method: 'POST', body })) {
  console.log(chunk.content)
}
```

When the response is a stream the response interceptor short-circuits entirely — no unwrapping, no business-code check, no success toast — and `_data` is left untouched, so callers no longer need `skipUnwrap` / `skipBusinessCheck`.

To plug into the AI SDK and `@nuxt/ui` Chat components, use [`createChatTransport`](https://nuxt.mhaibaraai.cn/en/docs/api/chat-transport) instead.

## API

### useApiStream()

#### Type Parameters

**T** (`unknown`): Type of a single chunk.

#### Returns

**status** (`Ref<ApiStreamStatus>`): 'idle' | 'streaming' | 'success' | 'error' | 'aborted'. Stays 'idle' until iteration starts.

**error** (`Ref<ApiError | Error | null>`): Error information; deliberate aborts are not written here.

**stream()** (`(url: string, options?: UseApiStreamOptions<T>) => AsyncGenerator<T>`): Starts the streaming request and yields chunks one by one.ParametersEndpoint name to use; defaults to defaultEndpoint.How to decode a single SSE event's data; return undefined to drop it. Defaults to a fault-tolerant JSON.parse.Failure callback; not triggered on deliberate aborts.Every other option is forwarded to ofetch: method, body, headers, query, responseType and so on.

**abort()** (`() => void`): Aborts the current stream and sets status to 'aborted'.

## Changelog

See commit history for [src/runtime/composables/useApiStream.ts](https://github.com/mhaibaraai/movk-nuxt/commits/main/src/runtime/composables/useApiStream.ts).


## Sitemap

See the full [sitemap](https://nuxt.mhaibaraai.cn/sitemap.md) for all pages.
