0Pricing
FastAPI Backend Development Bootcamp · 课时

流式响应与范围请求

使用 StreamingResponse 提供大文件,并支持 HTTP 范围请求以实现可恢复下载。

流式响应与范围请求 是 CoddyKit 上的免费 FastAPI Backend Development Bootcamp 课时。 这是第 2 节课,共 4 节。 你可以在下方免费阅读本课时的完整内容 — 然后在浏览器中使用内置代码编辑器和全天候 AI 导师进行实践。 这是 FastAPI Backend Development Bootcamp 学习路径的一部分,你的进度在网页和 CoddyKit 应用中同步。 FastAPI Backend Development Bootcamp 课程共包含 4 节课。

本课时的部分内容尚未翻译,以英文显示。

Why Stream Responses?

By default, returning a file from FastAPI means loading the entire payload into memory before sending it. For a 2 GB video that is a disaster: memory spikes, slow first byte, and crashes under concurrency.

Streaming solves this by sending the body in small chunks as they become available. The server holds only one chunk at a time, and the client starts receiving data almost immediately.

  • StreamingResponse — wraps a generator/iterator that yields bytes.
  • FileResponse — a convenience for serving a file from disk efficiently.
  • Range requests — let clients fetch only part of a file (seeking, resuming).

This lesson builds all three, ending with resumable downloads.

A Generator That Yields Bytes

Streaming starts with an iterable of bytes. The cleanest source is a Python generator that reads a file in fixed-size chunks instead of all at once.

Here is the core idea, isolated from any framework. The generator yields 1 MB at a time, so peak memory stays tiny no matter how large the file is.

def file_chunks(path, chunk_size=1024 * 1024):
    with open(path, "rb") as f:
        while True:
            chunk = f.read(chunk_size)
            if not chunk:
                break
            yield chunk


if __name__ == "__main__":
    import os
    with open("sample.bin", "wb") as f:
        f.write(b"x" * (3 * 1024 * 1024 + 17))

    total = 0
    pieces = 0
    for chunk in file_chunks("sample.bin"):
        total += len(chunk)
        pieces += 1
    print("bytes:", total)
    print("chunks:", pieces)
    os.remove("sample.bin")

StreamingResponse Basics

StreamingResponse takes any sync or async iterable of bytes (or strings) as its first argument. You set the media_type so the browser knows how to handle the body.

Notice we pass the generator object itself, not its result — FastAPI iterates it lazily while sending.

from fastapi import FastAPI
from fastapi.responses import StreamingResponse

app = FastAPI()


def file_chunks(path, chunk_size=1024 * 1024):
    with open(path, "rb") as f:
        while chunk := f.read(chunk_size):
            yield chunk


@app.get("/download/report")
def download_report():
    return StreamingResponse(
        file_chunks("report.pdf"),
        media_type="application/pdf",
    )

Setting Content-Disposition

To make the browser download a file (instead of trying to display it) and choose a filename, send a Content-Disposition header.

  • attachment — force a download dialog.
  • inline — display in the browser if possible.
  • filename="..." — the suggested name.

Pass custom headers via the headers argument of StreamingResponse.

from fastapi import FastAPI
from fastapi.responses import StreamingResponse

app = FastAPI()


def file_chunks(path, chunk_size=1024 * 1024):
    with open(path, "rb") as f:
        while chunk := f.read(chunk_size):
            yield chunk


@app.get("/export/users.csv")
def export_users():
    headers = {
        "Content-Disposition": 'attachment; filename="users.csv"'
    }
    return StreamingResponse(
        file_chunks("users.csv"),
        media_type="text/csv",
        headers=headers,
    )

Streaming Generated Data On the Fly

Streaming is not limited to files on disk. You can generate the body incrementally — for example, exporting a huge CSV row by row from a database cursor without ever building the full string in memory.

The generator below yields one CSV line at a time. Each yield is flushed to the client as soon as it is produced.

import csv
import io


def csv_stream(rows):
    buffer = io.StringIO()
    writer = csv.writer(buffer)
    writer.writerow(["id", "name", "score"])
    yield buffer.getvalue()

    for row in rows:
        buffer.seek(0)
        buffer.truncate(0)
        writer.writerow(row)
        yield buffer.getvalue()


if __name__ == "__main__":
    data = [(i, f"user{i}", i * 10) for i in range(5)]
    output = "".join(csv_stream(data))
    print(output, end="")

FileResponse: The Easy Path

When you just need to serve an existing file from disk, FileResponse is simpler than wiring a generator. Starlette streams it efficiently and sets sensible headers for you.

  • Guesses Content-Type from the extension.
  • Sets Content-Length automatically.
  • Adds ETag and Last-Modified for caching.
  • Crucially, it already supports range requests out of the box.

For static, on-disk files, prefer FileResponse over a manual StreamingResponse.

from fastapi import FastAPI
from fastapi.responses import FileResponse

app = FastAPI()


@app.get("/media/{name}")
def serve_media(name: str):
    return FileResponse(
        path=f"media/{name}",
        filename=name,
        media_type="video/mp4",
    )

What Is an HTTP Range Request?

A range request lets a client ask for only part of a resource. The browser sends:

Range: bytes=1048576-2097151

The server replies with status 206 Partial Content and these headers:

  • Content-Range: bytes 1048576-2097151/5242880 — the slice and the total size.
  • Content-Length — the length of just this slice.
  • Accept-Ranges: bytes — advertises that ranges are supported.

This powers video seeking (jump to minute 5 without downloading minutes 0–4) and resumable downloads (continue from where a dropped connection stopped).

Parsing the Range Header

To support ranges manually, you must parse the Range header. The format is bytes=start-end where either side may be omitted:

  • bytes=500-999 — bytes 500 through 999.
  • bytes=500- — from 500 to the end.
  • bytes=-500 — the last 500 bytes (suffix range).

This standalone parser returns inclusive (start, end) offsets for a given file size.

def parse_range(header, file_size):
    units, _, rng = header.partition("=")
    if units.strip() != "bytes":
        raise ValueError("only byte ranges supported")
    start_s, _, end_s = rng.strip().partition("-")

    if start_s == "":
        # suffix range: last N bytes
        length = int(end_s)
        start = max(file_size - length, 0)
        end = file_size - 1
    else:
        start = int(start_s)
        end = int(end_s) if end_s else file_size - 1

    end = min(end, file_size - 1)
    if start > end:
        raise ValueError("unsatisfiable range")
    return start, end


if __name__ == "__main__":
    size = 5000
    print(parse_range("bytes=0-499", size))
    print(parse_range("bytes=4500-", size))
    print(parse_range("bytes=-100", size))

Reading Just the Requested Slice

Once you have (start, end), you must stream only that window. Use file.seek(start) to jump to the offset, then read in chunks while counting down the remaining bytes so you never overshoot end.

This generator yields exactly end - start + 1 bytes.

def ranged_chunks(path, start, end, chunk_size=1024 * 1024):
    remaining = end - start + 1
    with open(path, "rb") as f:
        f.seek(start)
        while remaining > 0:
            chunk = f.read(min(chunk_size, remaining))
            if not chunk:
                break
            remaining -= len(chunk)
            yield chunk


if __name__ == "__main__":
    import os
    with open("blob.bin", "wb") as f:
        f.write(bytes(range(256)) * 40)  # 10240 bytes

    got = b"".join(ranged_chunks("blob.bin", 100, 199, chunk_size=32))
    print("length:", len(got))
    print("first byte:", got[0])
    os.remove("blob.bin")

A Full Range-Aware Endpoint

Now we combine everything into one FastAPI endpoint that handles both full and partial downloads:

  • No Range header → stream the whole file with 200 OK.
  • Valid Range → stream the slice with 206 Partial Content plus Content-Range.
  • Unsatisfiable range → return 416 with a Content-Range: bytes */size header.

Always advertise Accept-Ranges: bytes so clients know seeking is allowed.

import os
from fastapi import FastAPI, Request
from fastapi.responses import StreamingResponse, Response

app = FastAPI()
VIDEO = "media/movie.mp4"


def ranged_chunks(path, start, end, chunk_size=1024 * 1024):
    remaining = end - start + 1
    with open(path, "rb") as f:
        f.seek(start)
        while remaining > 0 and (chunk := f.read(min(chunk_size, remaining))):
            remaining -= len(chunk)
            yield chunk


@app.get("/video")
def stream_video(request: Request):
    size = os.path.getsize(VIDEO)
    range_header = request.headers.get("range")

    if range_header is None:
        return StreamingResponse(
            ranged_chunks(VIDEO, 0, size - 1),
            media_type="video/mp4",
            headers={"Accept-Ranges": "bytes",
                     "Content-Length": str(size)},
        )

    start, end = parse_range(range_header, size)
    headers = {
        "Content-Range": f"bytes {start}-{end}/{size}",
        "Accept-Ranges": "bytes",
        "Content-Length": str(end - start + 1),
    }
    return StreamingResponse(
        ranged_chunks(VIDEO, start, end),
        status_code=206,
        media_type="video/mp4",
        headers=headers,
    )

Async Streaming and Cleanup

For non-blocking I/O under load, use an async generator. Reading the disk inside a thread pool keeps the event loop free; libraries like aiofiles do this for you.

Two important rules:

  • Streaming runs after your function returns, so resources opened inside the generator must be released in a finally block.
  • If the client disconnects mid-stream, FastAPI raises inside the generator — that finally still runs, so handles never leak.
import aiofiles
from fastapi import FastAPI
from fastapi.responses import StreamingResponse

app = FastAPI()


async def async_chunks(path, chunk_size=1024 * 1024):
    f = await aiofiles.open(path, "rb")
    try:
        while chunk := await f.read(chunk_size):
            yield chunk
    finally:
        await f.close()


@app.get("/async-download")
async def async_download():
    return StreamingResponse(
        async_chunks("big.bin"),
        media_type="application/octet-stream",
        headers={"Accept-Ranges": "bytes"},
    )

Quick Check

A client sends Range: bytes=2000-2999 for a 10000-byte file. Which status code and headers should your endpoint return for a correct partial download?

Recap

You can now serve large media efficiently and support resumable, seekable downloads.

  • StreamingResponse wraps a byte iterable so peak memory equals one chunk, not the whole file.
  • FileResponse is the easy path for on-disk files and already supports ranges plus caching headers.
  • A range request sends Range: bytes=start-end; reply with 206, Content-Range, a slice-sized Content-Length, and Accept-Ranges: bytes.
  • Parse the header (including suffix bytes=-N), seek(start), and read while tracking remaining bytes so you never overshoot.
  • Use async generators with a finally block to close handles even when clients disconnect mid-stream.

常见问题解答

「流式响应与范围请求」课时是免费的吗?

是的 — 「流式响应与范围请求」的完整文本可在网页上免费阅读。要进行交互式练习(内置代码编辑器和全天候 AI 导师)并解锁 FastAPI Backend Development Bootcamp 课程的其余内容,请升级到 CoddyKit PRO。 FastAPI Backend Development Bootcamp 课程共包含 4 节课。

「流式响应与范围请求」这节课中我会学到什么?

使用 StreamingResponse 提供大文件,并支持 HTTP 范围请求以实现可恢复下载。 你通过在浏览器中直接运行的动手代码来练习 FastAPI Backend Development Bootcamp,全天候 AI 导师会在你学习这节课的过程中回答你的问题。

学习 FastAPI Backend Development Bootcamp 需要有经验吗?

无需任何先前经验。CoddyKit 上的 FastAPI Backend Development Bootcamp 课程适合初学者到高级学习者,你可以从这里开始或从头开始,按照自己的节奏学习。 这是第 2 节课,共 4 节。

「流式响应与范围请求」课时需要多长时间?

大多数 CoddyKit 课程大约需要 5–10 分钟。每节课都很精短且互动,所以你能稳步进步,并在网页和应用中从离开的地方继续。

我能在这节 FastAPI Backend Development Bootcamp 课中编写并运行代码吗?

能。每节 FastAPI Backend Development Bootcamp 课都包含内置代码编辑器,你可以在浏览器中直接编写并运行真实代码,并获得即时 AI 反馈 — 无需本地设置。

此课程中的所有课时

  1. 多部分上传与内容验证
  2. 流式响应与范围请求
  3. 将存储卸载到兼容 S3 的存储桶
  4. 异步图像与文档转换
← 返回 FastAPI Backend Development Bootcamp