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

FastAPI上传文件至Firebase Storage时Content-Type自动变为text/plain求助

问题:FastAPI上传文件到Firebase Storage时被存储为text/plain,修改ContentType报错

我用FastAPI开发文件上传端点,通过Postman测试,需要把文件上传到Firebase Storage,但文件总是被存为text/plain。尝试显式设置ContentType时,出现了不匹配的错误。

端点代码

import os
import uuid
from typing import Optional

import requests
from dotenv import load_dotenv
from fastapi import APIRouter
from fastapi import status as response_status
from fastapi.encoders import jsonable_encoder
from fastapi.responses import JSONResponse
from firebase_admin import credentials, firestore, initialize_app, storage
from starlette.datastructures import UploadFile
from starlette.requests import Request

@router.post("/saveEmailContent/")
async def save_email_content(request: Request):

    # Get the email data from the request
    form_data = await request.form()

    # Get the sender, recipient, subject and body from the email
    sender_email_id = form_data.get('sender')
    recipient_email_id = form_data.get('recipient')
    subject = form_data.get('subject')
    email_body = form_data.get('body-plain')
    attachment_count = form_data.get('attachment-count', 0)
    to_update_attachment_count = attachment_count == 0

    # Get the user and store ID from the recipient email ID 
    user_ref = firestore.client().collection('emailIds').where('emailId', '==', recipient_email_id).get()
    if len(user_ref) == 0:
        return JSONResponse(
            status_code=response_status.HTTP_404_NOT_FOUND,
            content={"message": "User not found"},
        )
    user_id = user_ref[0].to_dict().get('userId')
    store_id = user_ref[0].to_dict().get('storeId')

    # Add the email ID to the store document in the senderIds array 
    store_ref = firestore.client().collection('stores').document(store_id)
    store_ref.update({'senderIds': firestore.ArrayUnion([sender_email_id]), "updatedOn": firestore.SERVER_TIMESTAMP})

    # Upload attachments to Google Cloud Storage
    attachment_urls = []

    bucket_name = os.getenv("EMAIL_ATTACHMENT_STORAGE_BUCKET_NAME")
    folder_name = os.getenv("EMAIL_ATTACHMENT_STORAGE_FOLDER_NAME")

    for field_name, field_value in form_data.items():
        if isinstance(field_value, UploadFile):
            
            # Get the attachment from the request
            attachment = await field_value.read()

            # Update attachment count if update_flag is set to True 
            if to_update_attachment_count:
                attachment_count += 1

            # Generate a unique filename for each attachment
            unique_filename = f"{folder_name}/{uuid.uuid4().hex}"

            # Create a storage reference with the unique filename
            storage_ref = storage.bucket(bucket_name).blob(unique_filename)

            # Set the content type based on the original attachment's content type
            storage_ref.content_type = field_value.content_type
            print(f"Content-Type: {field_value.content_type}")

            # Upload the attachment to Firebase Cloud Storage
            storage_ref.upload_from_string(attachment)

            # Get the public URL of the uploaded attachment
            attachment_url = storage_ref.public_url
            print(attachment_url)

            # Add the attachment URL to the list
            attachment_urls.append(attachment_url)

    # Combine the data in a dictionary 
    email_data = {
        "senderEmailId": sender_email_id,
        "recipientEmailId": recipient_email_id,
        "subject": subject,
        "blurb": email_body[:25],
        "recepientUserId": user_id,
        "storeId": store_id,
        "attachment_count": attachment_count,
    }

    # Add the email data to the database 
    db = firestore.client()
    update_time, email_ref = db.collection('emails').add(email_data)

    # Add the email content to the email document
    email_ref.collection('content').add({
        'body': email_body,
        "attachments": attachment_urls,
    })
    email_data["attachments"] = attachment_urls

    return JSONResponse(
        status_code=response_status.HTTP_200_OK,
        content={"message": "Email content saved successfully", "emailData": email_data},
    )

Postman请求

Postman请求截图

错误信息

{
    "errors": [
        {
            "message": "Content-Type specified in the upload (text/plain) does not match Content-Type specified in metadata (image/jpeg). If it was a simple upload (uploadType=media), the Content-Type was specified as a bare header. If it was a multipart upload (uploadType=multipart), then the Content-Type was specified in the second part of the multipart. If it was a resumable upload (uploadType=resumable), then the Content-Type was specified with the X-Upload-Content-Type header with the start of the resumable session.",
            "domain": "global",
            "reason": "invalid"
        }
    ]
}

解决方案

问题出在upload_from_string方法的默认行为上:当使用该方法上传内容时,它默认会把请求的Content-Type设为text/plain,而你先设置了storage_ref.content_type,导致元数据和上传请求的Content-Type不匹配,触发错误。

有两种修复方式:

方式1:用upload_from_file替代upload_from_string

既然已经拿到了UploadFile对象,直接使用upload_from_file更合适,它会自动处理文件的Content-Type,不需要手动读取内容:

修改上传部分的代码:

if isinstance(field_value, UploadFile):
    # 不需要手动read(),直接传入文件对象
    if to_update_attachment_count:
        attachment_count += 1

    unique_filename = f"{folder_name}/{uuid.uuid4().hex}"
    storage_ref = storage.bucket(bucket_name).blob(unique_filename)
    
    # 直接用upload_from_file,传入文件对象和Content-Type
    storage_ref.upload_from_file(
        field_value.file,
        content_type=field_value.content_type
    )
    
    attachment_url = storage_ref.public_url
    attachment_urls.append(attachment_url)

方式2:在upload_from_string中指定content_type参数

如果坚持用upload_from_string,需要在方法调用时直接传入content_type,而不是先设置storage_ref.content_type:

修改代码:

if isinstance(field_value, UploadFile):
    attachment = await field_value.read()
    
    if to_update_attachment_count:
        attachment_count += 1

    unique_filename = f"{folder_name}/{uuid.uuid4().hex}"
    storage_ref = storage.bucket(bucket_name).blob(unique_filename)
    
    # 直接在upload_from_string里指定content_type
    storage_ref.upload_from_string(
        attachment,
        content_type=field_value.content_type
    )
    
    attachment_url = storage_ref.public_url
    attachment_urls.append(attachment_url)

另外,检查Postman中的文件上传设置:确保在选择文件时,Postman自动识别了正确的Content-Type,或者手动指定对应的类型(比如图片选image/jpeg),避免前端传入错误的Content-Type值。


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

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.07.20 09:47:11