Python-Bottle上传S3及写入PostgreSQL RDS时出现500内部服务器错误
问题概述
在EC2实例部署的Python-Bottle服务,原本可正常将图片上传至S3存储桶;添加PostgreSQL RDS读写逻辑后,上传操作触发500内部服务器错误,文件仅保存至EC2本地user_uploads目录,未上传至S3。已确认EC2与S3、RDS的通信规则均开放。
数据库建表语句:
CREATE SCHEMA bottletube; SET SCHEMA 'bottletube'; CREATE TABLE IF NOT EXISTS image_uploads ( id int GENERATED ALWAYS AS IDENTITY PRIMARY KEY, url VARCHAR(20) NOT NULL, category VARCHAR(64) );
相关Python核心代码(简化后):
# 上传路由逻辑 @route('/upload', method='POST') def do_upload_post(): # ... 字段校验、本地文件保存逻辑 ... # 上传至S3 data = open(SAVE_PATH + save_filename, 'rb') s3_resource.Bucket(BUCKET_NAME).put_object(Key=f'user_uploads/{save_filename}', Body=data, Metadata={'Content-Type': content_type}, ACL='public-read') # 写入数据库 cursor.execute(f"INSERT INTO image_uploads (url, category) VALUES ('user_uploads/{save_filename}', '{category}');") connection.commit()
核心问题排查与修复
1. 数据库字段长度不足(直接触发500错误)
你的image_uploads表中url字段定义为VARCHAR(20),但插入的user_uploads/{save_filename}字符串长度远超20(例如user_uploads/photo_20240520.jpg就有27个字符),插入时会触发字段值过长的数据库错误,导致事务提交失败,抛出未捕获异常,最终Bottle返回500错误。
修复方法:修改url字段长度为足够容纳S3对象路径的长度:
ALTER TABLE bottletube.image_uploads ALTER COLUMN url TYPE VARCHAR(255);
2. SQL字符串拼接导致语法错误与注入风险
直接用f-string拼接SQL语句,当category包含单引号(如'nature's beauty')或特殊字符时,会触发SQL语法错误,同时存在SQL注入风险,这也是引发500错误的常见原因。
修复方法:使用参数化查询代替字符串拼接:
# 替换原INSERT语句 cursor.execute( "INSERT INTO image_uploads (url, category) VALUES (%s, %s);", (f'user_uploads/{save_filename}', category) ) connection.commit()
3. 路径拼接错误导致S3读取文件失败
当前代码中SAVE_PATH + save_filename未处理路径分隔符(若SAVE_PATH为user_uploads且无末尾斜杠,会拼接成user_uploadstest.jpg),导致本地文件路径错误,S3读取文件时触发异常,中断上传流程。
修复方法:使用os.path.join安全拼接路径:
# 本地文件保存路径 local_file_path = os.path.join(SAVE_PATH, save_filename) with open(local_file_path, 'wb') as open_file: open_file.write(upload.file.read()) # S3读取路径 data = open(local_file_path, 'rb')
4. 缺失异常捕获与日志输出
当前代码在S3上传、DB写入步骤未添加异常捕获,无法定位具体错误原因。建议添加异常捕获并打印日志,方便快速排查:
# S3上传部分添加异常捕获 try: data = open(local_file_path, 'rb') s3_resource.Bucket(BUCKET_NAME).put_object(Key=f'user_uploads/{save_filename}', Body=data, Metadata={'Content-Type': content_type}, ACL='public-read') except Exception as e: print(f"S3上传失败: {str(e)}") error_messages.append(f"上传至S3失败: {str(e)}") return template('upload.tpl', name='Upload Image', error_messages=error_messages) finally: data.close() # DB写入部分添加异常捕获 try: cursor.execute( "INSERT INTO image_uploads (url, category) VALUES (%s, %s);", (f'user_uploads/{save_filename}', category) ) connection.commit() except Exception as e: connection.rollback() print(f"数据库写入失败: {str(e)}") error_messages.append(f"保存至数据库失败: {str(e)}") return template('upload.tpl', name='Upload Image', error_messages=error_messages)
5. 全局数据库连接/游标线程安全隐患
Bottle默认单线程运行,但全局的connection和cursor在后续切换多线程服务器(如waitress)时会引发线程安全问题,甚至导致随机错误。
优化建议:在每个请求中创建局部数据库连接/游标,或使用连接池:
@route('/home') @route('/') def home(): items = [] # 使用局部连接与游标 with psycopg2.connect(user="postgres", host="your-rds-endpoint", password="your-password", database="bottletube") as conn: with conn.cursor() as cur: cur.execute('SET SCHEMA \'bottletube\'') cur.execute('SELECT * FROM image_uploads ORDER BY id') for record in cur.fetchall(): items.append({'id': record[0], 'filename': record[1], 'category': record[2]}) return template('home.tpl', name='BoTube Home', items=items)
验证步骤
- 先修改数据库
url字段长度为255 - 替换SQL语句为参数化查询
- 修复路径拼接逻辑
- 添加异常捕获与日志打印
- 重启Bottle服务后重新测试上传
内容的提问来源于stack exchange,提问作者Oliver-René Goltsche

