# Transfer files and streams

Use [`ky`](https://bench-ky-61.atloria.app/p/bench-ky-61-6TqdzXtmKC/developer/ky#default) to send files or generated byte streams and consume downloads with progress callbacks. Ky builds on Fetch: pass standard `FormData`, `Blob`, or `ReadableStream` values through `body`, rather than serializing them with `json`.

The samples run in a browser project with `ky` installed:

```bash
npm install ky
```

Replace the example API URLs with endpoints that accept your uploads or serve your files. Cross-origin endpoints need the appropriate CORS configuration. Upload progress and streaming uploads also need request stream support; Chromium-based browsers require HTTP/2 for HTTPS connections.

## 1. Upload a multipart file

Pass a `FormData` instance to `ky.post()`. This sample sends a small text file and a description, logs upload progress where supported, and logs the response status after the request succeeds.

```ts
import ky from 'ky';

const file = new File(['Transfer example\n'], 'notes.txt', {
	type: 'text/plain',
});
const formData = new FormData();
formData.append('file', file);
formData.append('description', 'Example notes');

async function uploadFile() {
	const response = await ky.post('https://api.example.com/uploads', {
		body: formData,
		retry: {limit: 0},
		onUploadProgress: (progress, chunk) => {
			console.log({
				percent: progress.percent * 100,
				transferredBytes: progress.transferredBytes,
				totalBytes: progress.totalBytes,
				chunkBytes: chunk.byteLength,
			});
		},
	});

	console.log('Upload response status:', response.status);
}

uploadFile().catch(console.error);
```

Leave `Content-Type` unset so Fetch generates `multipart/form-data` with the boundary matching the encoded body. An explicit `Content-Type` in `headers` takes precedence; Ky does not repair an explicitly supplied multipart header.

If a `beforeRequest` hook replaces the form with a new `FormData`, delete `request.headers`'s `content-type` entry before returning `new Request(request, {body: newFormData})`. This lets the request constructor generate a boundary for the replacement body.

For text-only fields that your endpoint expects as `application/x-www-form-urlencoded`, use `URLSearchParams` instead. This sends two encoded fields and returns the response without attempting to parse an upload receipt:

```ts
import ky from 'ky';

const fields = new URLSearchParams();
fields.set('food', 'fries');
fields.set('drink', 'icetea');

async function submitForm() {
	const response = await ky.post('https://api.example.com/orders', {
		body: fields,
	});

	console.log('Form response status:', response.status);
}

submitForm().catch(console.error);
```

## 2. Send a streaming body without retry buffering

Pass a `ReadableStream<Uint8Array>` through `body`. Ky sets `duplex: 'half'` for you in environments with request stream support. This sample sends two text chunks and logs the response status after the request succeeds:

```ts
import ky from 'ky';

const encoder = new TextEncoder();
const stream = new ReadableStream<Uint8Array>({
	start(controller) {
		controller.enqueue(encoder.encode('first line\n'));
		controller.enqueue(encoder.encode('second line\n'));
		controller.close();
	},
});

async function uploadStream() {
	const response = await ky.post('https://api.example.com/uploads/raw', {
		body: stream,
		headers: {'content-type': 'text/plain'},
		retry: {limit: 0},
	});

	console.log('Stream upload response status:', response.status);
}

uploadStream().catch(console.error);
```

Set `retry: {limit: 0}` when you do not need replay. With a positive retry limit, Ky clones the request before sending it. Cloning a streaming body uses `tee()` and buffers the body in memory for a possible retry. Disabling retries skips that clone, which matters for large uploads.

## 3. Consume a download and report progress

Consume the response body to drive download progress. Calling `await ky.get(url)` alone does not demonstrate a completed download; the callback wraps the response stream.

This sample reads each chunk without collecting the whole file in a `Blob`. It logs progress and a completion message for an accepted download, and cancels the reader if the body exceeds a 20 MiB application limit. The limit counts the bytes you actually read, not a server-provided size estimate.

```ts
import ky from 'ky';

async function downloadFile() {
	const maxBytes = 20 * 1024 * 1024;
	const response = await ky.get('https://api.example.com/files/report.csv', {
		onDownloadProgress: (progress, chunk) => {
			console.log({
				percent: progress.percent * 100,
				transferredBytes: progress.transferredBytes,
				totalBytes: progress.totalBytes,
				chunkBytes: chunk.byteLength,
			});
		},
	});

	if (!response.body) {
		throw new Error('The response has no body stream');
	}

	const reader = response.body.getReader();
	let receivedBytes = 0;

	try {
		while (true) {
			const {done, value} = await reader.read();
			if (done) {
				break;
			}

			receivedBytes += value.byteLength;
			if (receivedBytes > maxBytes) {
				await reader.cancel('Download exceeds the application limit');
				throw new Error(`Download exceeds ${maxBytes} bytes`);
			}

			console.log('Accepted chunk bytes:', value.byteLength);
		}

		console.log('Download consumed:', receivedBytes, 'bytes');
	} finally {
		reader.releaseLock();
	}
}

downloadFile().catch(console.error);
```

If you need the complete file in memory instead, call `await ky.get(url, {onDownloadProgress: callback}).blob()` with your callback. The shortcut returns a `Blob` after consuming the body; it buffers the complete download rather than processing chunks individually.

## Options and progress values

The callbacks receive a [`Progress`](https://bench-ky-61.atloria.app/p/bench-ky-61-6TqdzXtmKC/developer/ky#progress) object and a `Uint8Array` chunk. `percent` ranges from `0` to `1`; multiply it by `100` for display. `totalBytes` is an estimate, not a size limit. Download totals start from `Content-Length`; multipart upload sizes are approximate, and a streaming upload may have no known total.

| Option | Type | Default | What it does |
| --- | --- | --- | --- |
| `body` | `BodyInit \| null` | Not set | Sends a standard Fetch body. |
| `onUploadProgress` | `(progress: Progress, chunk: Uint8Array) => void` | Not set | Reports upload-stream progress where supported. |
| `onDownloadProgress` | `(progress: Progress, chunk: Uint8Array) => void` | Not set | Reports progress as you consume the response stream. |
| `retry.limit` | `number` | `2` | Controls retries; `0` skips request cloning for replay. |
| `maxResponseSize` (next release only) | `number` | `Infinity` | Limits consumed response-body bytes. |

Progress reaches `1` when the wrapped stream finishes. Do not treat upload progress reaching `1` as the server's acknowledgement: await the response separately, as the upload samples do. For an empty body stream, the completion callback receives an empty chunk; a response with no body stream has no download callback.

## Response-size limits and pitfalls

- **Response-size limits:** The reader loop above enforces an application limit while processing download chunks with the released API; see [Handle request errors](https://bench-ky-61.atloria.app/p/bench-ky-61-6TqdzXtmKC/developer/handle-request-errors) for next-release size enforcement and error handling.
- **Upload support:** `onUploadProgress` is silently ignored without request stream support, with `keepalive: true`, or with `mode: 'no-cors'`. Ignoring the callback does not make a streaming body compatible with those environments; use a compatible non-stream body when needed.
- **Download support:** `onDownloadProgress` requires response streams. Ky throws if `ReadableStream` support is missing.
- **Error responses:** Ky throws [`HTTPError`](https://bench-ky-61.atloria.app/p/bench-ky-61-6TqdzXtmKC/developer/ky-httperror#httperror) for non-2xx responses by default. Its response body is consumed to populate `error.data`; use that property rather than reading `error.response` again. See [Handle request errors](https://bench-ky-61.atloria.app/p/bench-ky-61-6TqdzXtmKC/developer/handle-request-errors).

## Related

- [Retry failed requests](https://bench-ky-61.atloria.app/p/bench-ky-61-6TqdzXtmKC/developer/retry-failed-requests) explains method eligibility and retry policy.
- [Cancel requests](https://bench-ky-61.atloria.app/p/bench-ky-61-6TqdzXtmKC/developer/cancel-requests) shows how to abort transfers with a signal.
- [Request lifecycle](https://bench-ky-61.atloria.app/p/bench-ky-61-6TqdzXtmKC/developer/request-lifecycle) explains where hooks can replace requests and responses.
