---
title: "useDownloadWithProgress"
description: "File download composable with real-time progress monitoring, supporting JSON business validation, POST export, abort and indeterminate progress."
canonical_url: "https://nuxt.mhaibaraai.cn/en/docs/composables/use-download-with-progress"
---
# useDownloadWithProgress

> File download composable with real-time progress monitoring, supporting JSON business validation, POST export, abort and indeterminate progress.

## Usage

`useDownloadWithProgress` is built on the native `fetch` + `ReadableStream` and reuses `@movk/nuxt`'s endpoint, auth, business validation, Toast and `movk:api:*` hook system.

When the backend returns `application/json`, business validation is applied and error responses are **not** written to a file as binary. When binary data is returned, progress is calculated from `content-length`; when `content-length` is missing, `progress` is `null` and the UI can switch to indeterminate accordingly.

```vue
<script setup lang="ts">
const { progress, status, error, download, abort } = useDownloadWithProgress()

async function handleDownload() {
  const { aborted, error: err } = await download('/api/export/report', {
    filename: 'monthly-report.pdf',
    onSuccess: name => console.log('Done', name)
  })
  if (aborted) return
  if (err) console.error(err)
}
</script>

<template>
  <UButton :loading="status === 'pending'" @click="handleDownload">Download</UButton>
  <UProgress
    v-if="status === 'pending'"
    :model-value="progress ?? undefined"
    :max="100"
  />
  <UButton v-if="status === 'pending'" @click="abort">Cancel</UButton>
</template>
```

> [!NOTE]
> 
> Use this for large file exports, bulk downloads and similar scenarios. For small files where progress is not needed, use 
> 
> $api
> 
>  directly.

## Basic Download

When `content-length` is known, progress updates proportionally; `abort()` stops the stream read:

```vue [ComposablesUseDownloadProgressBasicExample.vue]
<script setup lang="ts">
const STATUS = {
  idle: { color: 'neutral', label: '空闲' },
  pending: { color: 'info', label: '传输中' },
  success: { color: 'success', label: '完成' },
  error: { color: 'error', label: '失败' },
  aborted: { color: 'warning', label: '已取消' }
} as const

const basic = useDownloadWithProgress()
</script>

<template>
  <div class="flex flex-col gap-3">
    <div class="flex items-center gap-2">
      <UButton
        :loading="basic.status.value === 'pending'"
        icon="i-lucide-download"
        @click="basic.download('/download/large')"
      >
        开始下载
      </UButton>
      <UButton
        v-if="basic.status.value === 'pending'"
        color="error"
        variant="soft"
        icon="i-lucide-x"
        @click="basic.abort"
      >
        中止
      </UButton>
      <UBadge :color="STATUS[basic.status.value].color" variant="subtle">
        {{ basic.progress.value ?? 0 }}% · {{ STATUS[basic.status.value].label }}
      </UBadge>
    </div>
    <UProgress :model-value="basic.progress.value ?? undefined" :max="100" />
    <UAlert
      v-if="basic.error.value"
      color="error"
      variant="subtle"
      :description="basic.error.value.message"
    />
  </div>
</template>
```

## Filename Resolution

Priority order:

1. `options.filename`
2. Response header `Content-Disposition` (supports RFC 5987 encoding)
3. Last segment of the URL pathname (query string is automatically stripped)
4. `'download'`

```ts
await download('/api/export?token=abc')
// Automatically uses "export" as the fallback filename; query string does not pollute the name
```

## Indeterminate Progress

When the response is missing `content-length` (e.g. chunked transfer encoding), `progress` is `null`. Using nullish-coalescing to map it to `UProgress`'s `:model-value` automatically activates the indeterminate state:

```vue [ComposablesUseDownloadProgressChunkedExample.vue]
<script setup lang="ts">
const STATUS = {
  idle: { color: 'neutral', label: '空闲' },
  pending: { color: 'info', label: '传输中' },
  success: { color: 'success', label: '完成' },
  error: { color: 'error', label: '失败' },
  aborted: { color: 'warning', label: '已取消' }
} as const

const chunked = useDownloadWithProgress()
</script>

<template>
  <div class="flex flex-col gap-3">
    <div class="flex items-center gap-2">
      <UButton
        :loading="chunked.status.value === 'pending'"
        icon="i-lucide-download"
        @click="chunked.download('/download/chunked')"
      >
        开始下载
      </UButton>
      <UBadge :color="STATUS[chunked.status.value].color" variant="subtle">
        {{ chunked.progress.value === null ? '不确定' : `${chunked.progress.value}%` }}
        · {{ STATUS[chunked.status.value].label }}
      </UBadge>
    </div>
    <UProgress :model-value="chunked.progress.value ?? undefined" :max="100" />
  </div>
</template>
```

## Custom Filename

`options.filename` overrides the response header and URL inference to force a specific download filename:

```vue [ComposablesUseDownloadProgressCustomExample.vue]
<script setup lang="ts">
const STATUS = {
  idle: { color: 'neutral', label: '空闲' },
  pending: { color: 'info', label: '传输中' },
  success: { color: 'success', label: '完成' },
  error: { color: 'error', label: '失败' },
  aborted: { color: 'warning', label: '已取消' }
} as const

const custom = useDownloadWithProgress()
</script>

<template>
  <div class="flex flex-col gap-3">
    <div class="flex items-center gap-2">
      <UButton
        :loading="custom.status.value === 'pending'"
        icon="i-lucide-file-down"
        @click="custom.download('/download/large', { filename: 'custom-name.bin' })"
      >
        自定义文件名下载
      </UButton>
      <UBadge :color="STATUS[custom.status.value].color" variant="subtle">
        {{ STATUS[custom.status.value].label }}
      </UBadge>
    </div>
    <UAlert
      v-if="custom.error.value"
      color="error"
      variant="subtle"
      :description="custom.error.value.message"
    />
  </div>
</template>
```

## POST Export

`method: 'POST'` + `body` puts filter criteria in the request body; `Content-Type` is automatically set to `application/json`:

```vue [ComposablesUseDownloadProgressPostExample.vue]
<script setup lang="ts">
const STATUS = {
  idle: { color: 'neutral', label: '空闲' },
  pending: { color: 'info', label: '传输中' },
  success: { color: 'success', label: '完成' },
  error: { color: 'error', label: '失败' },
  aborted: { color: 'warning', label: '已取消' }
} as const

const postExport = useDownloadWithProgress()
</script>

<template>
  <div class="flex flex-col gap-3">
    <div class="flex items-center gap-2">
      <UButton
        :loading="postExport.status.value === 'pending'"
        icon="i-lucide-package"
        @click="postExport.download('/download/large', {
          method: 'POST',
          body: { ids: [1, 2, 3] }
        })"
      >
        POST 导出
      </UButton>
      <UBadge :color="STATUS[postExport.status.value].color" variant="subtle">
        {{ STATUS[postExport.status.value].label }}
      </UBadge>
    </div>
    <UAlert
      v-if="postExport.error.value"
      color="error"
      variant="subtle"
      :description="postExport.error.value.message"
    />
  </div>
</template>
```

## Abort Download

```ts
const { status, download, abort } = useDownloadWithProgress()

download('/api/export/large.zip')

function handleCancel() {
  if (status.value === 'pending') abort()
}
```

After aborting:

- `status` changes to `'aborted'`; `progress` resets to `0`
- `error` remains `null` (not treated as failure)
- `onError` is not called; no Toast is shown
- `download()` returns `{ data: null, error: null, aborted: true }`

## Authentication

The token is automatically read from the session and injected into the `Authorization` header. Override it in `headers` if needed (user value takes priority).

```ts
await download('/api/export/private.pdf', {
  headers: { 'X-Custom': 'value' }
})
```

## API

### useDownloadWithProgress<T>()

`useDownloadWithProgress<T = Blob>(): { progress, status, data, error, download, abort }`

#### Returns

**progress** (`Ref<number | null>`): Download progress, 0–100. null when content-length is missing; the UI should switch to indeterminate in that case.

**status** (`Ref<TransferStatus>`): Transfer status: 'idle' | 'pending' | 'success' | 'error' | 'aborted'.

**data** (`Ref<T | null>`): In the binary path, a Blob. In the JSON path, the unwrapped business data.

**error** (`Ref<ApiError | Error | null>`): Error object. Business errors are ApiError; HTTP/network errors are Error. null on abort.

**download()** (`(url: string, options?: DownloadWithProgressOptions) => Promise<TransferResult<T>>`): Execute the download.ParametersDefaults to 'GET'.Serialized to JSON for non-GET requests.User value takes priority over the auto-injected auth header.ReturnsPromise<TransferResult<T>>: { data: T | null, error: ApiError | Error | null, aborted: boolean }.

**abort()** (`() => void`): Abort the current download; status switches to 'aborted'.

## Changelog

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


## Sitemap

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