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
相关产品推荐
相关产品推荐

