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

Python脚本上传图片遇ConnectionResetError,但手动上传正常

图片上传脚本ConnectionResetError问题排查与解决

问题背景

编写了一段Python脚本用于自动上传图片到Django/DRF构建的API端点,后端部署在Railway服务器,图片存储至Cloudinary。手动上传图片到该端点可成功,但脚本执行时抛出ConnectionResetError,添加速率限制后问题仍未解决。将后端迁移到本地开发环境运行(仍连接Cloudinary),错误依旧,确定问题与Railway无关,怀疑是Cloudinary对文件数据格式或Content-Type请求头有特定要求。

上传脚本代码

import time
import requests
from pathlib import Path
from random import randint

def single_campus_upload(image_file, property_id):
    BASE_URL_CAMPUS = f'http://[redacted].up.railway.app/api/properties/{property_id}/images/'

    # Open the image file in read-binary mode using the `with` statement to automatically close it after reading
    with image_file.open(mode='rb') as f:
        # Read the contents of the file into a bytes object
        image_data = f.read()

    # Send the POST request to the API endpoint with the image data
    response = requests.post(BASE_URL_CAMPUS, data=image_data)

    # Check the status code to see if the request was successful
    if response.status_code >= 200 and response.status_code < 300:
        # Parse the JSON response and print it in a readable format
        print(response.json())
    else:
        # Print the status code and the raw response if the request was not successful
        print(f'Request failed with status code {response.status_code}: {response.text}')

错误追踪栈

ConnectionResetError                      Traceback (most recent call last)
File ~/anaconda3/lib/python3.9/site-packages/urllib3/connectionpool.py:703, in HTTPConnectionPool.urlopen(self, method, url, body, headers, retries, redirect, assert_same_host, timeout, pool_timeout, release_conn, chunked, body_pos, **response_kw)
    702 # Make the request on the httplib connection object.
--> 703 httplib_response = self._make_request(
    704     conn,
    705     method,
    706     url,
    707     timeout=timeout_obj,
    708     body=body,
    709     headers=headers,
    710     chunked=chunked,
    711 )
    713 # If we're going to release the connection in ``finally:``, then
    714 # the response doesn't need to know about the connection. Otherwise
    715 # it will also try to release it and we'll have a double-release
    716 # mess.

File ~/anaconda3/lib/python3.9/site-packages/urllib3/connectionpool.py:398, in HTTPConnectionPool._make_request(self, conn, method, url, timeout, chunked, **httplib_request_kw)
    397     else:
--> 398         conn.request(method, url, **httplib_request_kw)
    400 # We are swallowing BrokenPipeError (errno.EPIPE) since the server is
    401 # legitimately able to close the connection after sending a valid response.
    402 # With this behaviour, the received response is still readable.

File ~/anaconda3/lib/python3.9/site-packages/urllib3/connection.py:239, in HTTPConnection.request(self, method, url, body, headers)
    238     headers["User-Agent"] = _get_default_user_agent()
--> 239 super(HTTPConnection, self).request(method, url, body=body, headers=headers)

File ~/anaconda3/lib/python3.9/http/client.py:1285, in HTTPConnection.request(self, method, url, body, headers, encode_chunked)
   1284 """Send a complete request to the server."""
--> 1285 self._send_request(method, url, body, headers, encode_chunked)

File ~/anaconda3/lib/python3.9/http/client.py:1331, in HTTPConnection._send_request(self, method, url, body, headers, encode_chunked)
   1330     body = _encode(body, 'body')
--> 1331 self.endheaders(body, encode_chunked=encode_chunked)

File ~/anaconda3/lib/python3.9/http/client.py:1280, in HTTPConnection.endheaders(self, message_body, encode_chunked)
   1279     raise CannotSendHeader()
--> 1280 self._send_output(message_body, encode_chunked=encode_chunked)

File ~/anaconda3/lib/python3.9/http/client.py:1079, in HTTPConnection._send_output(self, message_body, encode_chunked)
   1077         chunk = f'{len(chunk):X}\r\n'.encode('ascii') + chunk \
   1078             + b'\r\n'
--> 1079     self.send(chunk)
...
    503 except MaxRetryError as e:
    504     if isinstance(e.reason, ConnectTimeoutError):
    505         # TODO: Remove this in 3.0.0: see #2811

ConnectionError: ('Connection aborted.', ConnectionResetError(104, 'Connection reset by peer'))

解决方案

问题根源在于请求格式不符合Django/DRF和Cloudinary的要求:直接用data参数传递二进制图片数据,既没有设置正确的Content-Type,也没有使用文件上传标准的multipart/form-data格式。修改后的脚本如下:

import time
import requests
from pathlib import Path
from random import randint

def single_campus_upload(image_file, property_id):
    BASE_URL_CAMPUS = f'http://[redacted].up.railway.app/api/properties/{property_id}/images/'

    # 使用files参数构建多部分表单数据,需匹配API端点接收的文件字段名
    with image_file.open(mode='rb') as f:
        # 第三个参数为文件Content-Type,根据实际图片类型调整
        files = {'image': (image_file.name, f, 'image/jpeg')}
        response = requests.post(BASE_URL_CAMPUS, files=files)

    if response.status_code >= 200 and response.status_code < 300:
        print(response.json())
    else:
        print(f'Request failed with status code {response.status_code}: {response.text}')

关键修改点

  • 替换data参数为files参数,构建符合multipart/form-data规范的请求,这是Django/DRF接收文件的标准方式
  • 指定文件的Content-Type(如image/jpeg、image/png),确保Cloudinary能正确识别文件类型
  • 直接传入打开的文件对象,避免一次性读取大文件到内存,提升传输效率

扩展优化

如果图片类型多样,可以使用mimetypes模块自动检测文件的Content-Type:

import mimetypes

def get_content_type(file_path):
    return mimetypes.guess_type(file_path)[0] or 'application/octet-stream'

# 在函数中调用
content_type = get_content_type(image_file)
files = {'image': (image_file.name, f, content_type)}

注意:需将files字典中的键(示例中的image)替换为后端DRF序列化器定义的文件字段名。

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

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.08.06 17:05:26