You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

如何使用FastAPI从Zip文件提供文件服务并处理异常?

从Zip文件提供文件服务的优雅实现(带异常处理)

我希望从Zip文件中提供文件服务,请问是否存在优雅且支持异常处理的实现方法?

我的实验尝试

1. 基础方案:加载全文件到内存,不适用于大文件

该方案会把整个文件内容读入内存,大文件场景下性能差。

import zipfile
from typing import Annotated, Any

from fastapi import FastAPI, Depends
from fastapi.responses import StreamingResponse, Response

app = FastAPI()

zip_file_path = "data.zip"
file_path = "index.html"

@app.get("/zip0")
async def zip0():
    with zipfile.ZipFile(zip_file_path, 'r') as zip_file:
        return Response(content=zip_file.read(file_path))

2. StreamingResponse直接使用:触发"read from closed file"报错

FastAPI的StreamingResponse本应适合流式传输,但这里因为with语句会提前关闭Zip文件和内部文件句柄,导致响应流读取时文件已关闭。

@app.get("/zip1")
async def zip1():
    with zipfile.ZipFile(zip_file_path, 'r') as zip_file:
        with zip_file.open(file_path) as file_like:
            return StreamingResponse(file_like)

3. 依赖注入Hack方案:可正常流式传输但不规范

把StreamingResponse放到依赖的yield中,虽然能保证文件句柄在传输完成后关闭,但属于不规范用法,且StreamingResponse本身不支持作为依赖注入类型。

def get_file_stream_from_zip():
    with zipfile.ZipFile(zip_file_path, 'r') as zip_file:
        with zip_file.open(file_path) as file_like:
            yield StreamingResponse(file_like)


@app.get("/zip2")
async def zip2(
        streaming_response: Annotated[Any, Depends(get_file_stream_from_zip)],
):
    return streaming_response

4. 折中方案:合规但无法处理异常

依赖中返回文件句柄,再用StreamingResponse包装,但异常(比如Zip文件不存在)会在响应头发送后才抛出,导致无法正确返回错误响应。

from io import BinaryIO

def get_file_from_zip():
    with zipfile.ZipFile(zip_file_path, 'r') as zip_file:
        with zip_file.open(file_path) as file_like:
            yield file_like


@app.get("/zip3")
async def zip3(
        file_like: Annotated[BinaryIO, Depends(get_file_from_zip)],
):
    return StreamingResponse(file_like)

优雅且支持异常处理的实现方案

核心思路是提前完成异常检查,自定义流式迭代器管理资源,确保异常发生在响应头发送前,同时正确关闭文件句柄。

import zipfile
from typing import AsyncGenerator
from fastapi import FastAPI, HTTPException
from fastapi.responses import StreamingResponse

app = FastAPI()

zip_file_path = "data.zip"
file_path = "index.html"

async def zip_file_streamer(zip_path: str, inner_file_path: str) -> AsyncGenerator[bytes, None]:
    zip_file = None
    inner_file = None
    try:
        zip_file = zipfile.ZipFile(zip_path, 'r')
        # 提前检查目标文件是否在Zip内
        if inner_file_path not in zip_file.namelist():
            raise HTTPException(status_code=404, detail="文件在压缩包中不存在")
        
        inner_file = zip_file.open(inner_file_path)
        # 分块读取,默认1MB块可根据需求调整
        chunk_size = 1024 * 1024
        while chunk := inner_file.read(chunk_size):
            yield chunk
    except zipfile.BadZipFile:
        raise HTTPException(status_code=400, detail="无效的压缩文件")
    except FileNotFoundError:
        raise HTTPException(status_code=404, detail="压缩文件不存在")
    finally:
        # 确保所有文件句柄被关闭
        if inner_file:
            inner_file.close()
        if zip_file:
            zip_file.close()

@app.get("/zip")
async def serve_zip_file():
    return StreamingResponse(zip_file_streamer(zip_file_path, file_path))

方案优势:

  • 流式传输:分块读取内容,不会占用大量内存,适配大文件场景
  • 完整异常处理:在响应头发送前完成所有前置检查,能返回标准HTTP错误响应
  • 安全资源管理:通过finally块确保文件句柄被关闭,避免资源泄漏
  • 规范实现:遵循FastAPI响应流设计,无依赖注入的不规范用法

内容的提问来源于stack exchange,提问作者j123b567

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.07.07 16:58:14