vbnet
{
'code': 7,
'msg': "400 Bad Request: The browser (or proxy) sent a request that this server could not understand. ('audio',)"
}
在日常开发中,使用 Python 的 requests
库上传文件是一个非常常见的操作,语法简洁使用简单。然而,正是在这种简洁的背后,隐藏着一些细节,一旦忽略,就可能导致一个令人困惑的错误:400 Bad Request
。
这篇文章记录了一个真实的错误案例,并剖析问题根源,复盘这个文件上传中的"坑"。
一段看似无懈可击的代码
我需要向一个 API 上传一个参考音频文件和一段文本,用于克隆音色进行配音,很自然地写出下面的代码:
python
import requests
# --- 问题代码 ---
# 准备数据
api_url = "http://127.0.0.1:9880/apitts"
data = {"text": "这是一段测试文本", "language": "zh"}
file_path = "my_audio.wav"
try:
# 读取文件二进制内容
with open(file_path, 'rb') as f:
audio_chunk = f.read()
# 构造 files 字典
files = {"audio": audio_chunk}
# 发送请求
response = requests.post(api_url, data=data, files=files)
print(response.json())
except Exception as e:
print(f"请求失败: {e}")
这段代码逻辑清晰:准备数据、读取文件、构造 files
字典、发送请求。然而,当运行它时,收到了一个来自服务器的冰冷回应:
json
{'code': 7, 'msg': "400 Bad Request: The browser (or proxy) sent a request that this server could not understand. ('audio',)"}
服务器抱怨说"无法理解这个请求",并且明确指向了 'audio'
部分。这是为什么呢?代码明明把文件的二进制数据传递过去了呀?
服务器到底需要什么?
这个问题的核心在于,对 requests
库如何构建 multipart/form-data
请求的理解出现了偏差。
当通过 HTTP 上传文件时,请求的格式通常是 multipart/form-data
,可以把它想象成一个快递包裹,里面有好几个独立包装的物品。
data
字典里的内容,就像是包裹里贴着"文本信息"标签的小件。files
字典里的内容,就像是包裹里贴着"文件"标签的大件。
关键在于,每一个"大件"(文件)不仅需要有内容本身(文件的二进制数据),还需要有一张"标签",上面至少要写着它的"文件名"(filename)。服务器需要根据这个文件名来识别和处理上传的文件。
在我错误的代码中:
python
files = {"audio": audio_chunk}
只把文件的"内容"(audio_chunk
)放了进去,却忘记了附上"文件名"这张标签。requests
库在缺少这个关键信息时,无法构建出一个完全符合 HTTP 规范的 multipart/form-data
请求。服务器收到这个"缺斤少两"的请求后,自然就感到困惑,于是返回了 400 Bad Request
。
两种正确的文件上传姿势
知道了问题所在,解决起来就非常简单了。只需要在构造 files
字典时,把文件名也一并提供给 requests
即可。
方案一:直接传递文件对象
这是最简洁、最高效的方式。不需要手动 read()
文件,直接把 open()
返回的文件句柄传给 requests
就行。
python
import requests
api_url = "http://127.0.0.1:9880/apitts"
data = {"text": "这是一段测试音频", "language": "zh"}
file_path = "my_audio.wav"
try:
with open(file_path, 'rb') as f:
# 'audio' 是表单字段名,f 是文件对象
# requests 会自动从 f 中提取文件名和内容
files = {"audio": f}
response = requests.post(api_url, data=data, files=files, timeout=60)
response.raise_for_status()
print(response.json())
except requests.exceptions.RequestException as e:
print(f"请求出错: {e}")
except FileNotFoundError:
print(f"文件未找到: {file_path}")
优点:
- 代码简洁:省去了手动读写文件的步骤。
- 内存高效 :
requests
会以数据流的方式读取文件,而不是一次性把整个大文件加载到内存中。这在上传大文件时至关重要。
方案二:使用元组提供详细信息
如果确实需要先将文件内容读入内存(比如,文件内容来自网络下载,或者需要预处理),可以使用元组来手动指定文件名和文件内容。
元组的格式是:(filename, file_content)
。
python
import requests
api_url = "http://127.0.0.1:9880/apitts"
data = {"text": "这是一段测试音频", "language": "zh"}
file_path = "my_audio.wav"
try:
with open(file_path, 'rb') as f:
audio_chunk = f.read()
# 使用元组来指定文件名和文件内容
files = {"audio": ('my_audio.wav', audio_chunk)}
response = requests.post(api_url, data=data, files=files, timeout=60)
response.raise_for_status()
print(response.json())
except requests.exceptions.RequestException as e:
print(f"请求出错: {e}")
这种方式同样能解决问题,并且提供了更高的灵活性。
让代码更健壮
掌握了正确的上传方法后,还可以做一些事情让代码变得更专业、更可靠。
-
明确指定 MIME 类型 : 有时候,服务器还需要知道文件的具体类型(MIME type),比如是
image/jpeg
还是audio/wav
。可以在元组中加入第三个元素来指定它。pythonfiles = {"audio": ('my_audio.wav', audio_chunk, 'audio/wav')} files = {"avatar": ('user.jpg', image_bytes, 'image/jpeg')}
-
完善的错误处理 : 网络请求和文件操作都可能失败。一个健壮的程序应该考虑到各种异常情况,比如文件不存在 (
FileNotFoundError
)、请求超时、服务器错误等。使用try...except
块并捕获requests.exceptions.RequestException
是一个很好的实践。 -
使用
response.raise_for_status()
: 在收到响应后,调用这个方法。如果响应的状态码是 4xx(客户端错误)或 5xx(服务器错误),它会主动抛出一个异常。这比自己写if response.status_code != 200:
要方便得多。
requests
库为我们屏蔽了复杂的 HTTP 协议细节,但我们仍需理解其基本工作原理:向服务器发送数据时,不仅仅是数据本身,描述数据的元信息(如文件名、内容类型)同样至关重要。
一个简单的规则:
- 当上传文件时,
files
字典的值要么是文件对象(推荐),要么是包含(文件名, 文件内容, ...)
的元组。