Flask请求上下文、响应与错误处理机制深度解析
本文基于Flask 3.0+版本编写,深入剖析Flask请求处理的核心机制,包括请求上下文管理、响应构建、错误处理及中间件模式,帮助开发者构建健壮的Web应用。
一、请求处理机制概述
1.1 Flask请求处理架构
Flask的请求处理机制基于WSGI规范,通过上下文管理器实现请求隔离和状态管理。
上下文栈
请求处理流程
HTTP请求
WSGI Server
Flask.call
创建RequestContext
创建ApplicationContext
执行before_request
路由匹配
执行视图函数
执行after_request
执行teardown_request
销毁ApplicationContext
销毁RequestContext
返回Response
_app_ctx_stack
_request_ctx_stack
1.2 核心组件关系
contains
references
produces
RequestContext
+Request request
+AppContext app_context
+dict session
+push()
+pop()
+copy()
AppContext
+Flask app
+dict url_adapter
+dict g
+push()
+pop()
Request
+str method
+str path
+dict args
+dict form
+dict json
+dict headers
+dict cookies
+dict files
+str environ
Response
+str data
+int status_code
+dict headers
+set_cookie()
+delete_cookie()
二、请求对象详解
2.1 Request对象结构
Flask的request对象是werkzeug.wrappers.Request的子类,封装了HTTP请求的所有信息。
python
"""
Flask Request对象详解
request是一个代理对象,指向当前请求上下文中的Request实例
"""
from flask import Flask, request
app = Flask(__name__)
@app.route('/request-info', methods=['GET', 'POST'])
def request_info():
"""
Request对象属性完整示例
"""
# ============================================
# 请求行信息
# ============================================
method = request.method # HTTP方法: 'GET', 'POST', etc.
url = request.url # 完整URL: 'http://localhost:5000/path?a=1'
base_url = request.base_url # 不含查询参数的URL
url_root = request.url_root # 应用根URL
path = request.path # 路径部分: '/path'
full_path = request.full_path # 路径+查询参数: '/path?a=1'
script_root = request.script_root # 脚本根路径
host = request.host # 主机名: 'localhost:5000'
host_url = request.host_url # 主机URL: 'http://localhost:5000/'
# ============================================
# 请求参数
# ============================================
args = request.args # URL查询参数
form = request.form # 表单数据
values = request.values # args + form 合并
json = request.json # JSON数据(自动解析)
data = request.data # 原始请求体
files = request.files # 上传文件
# ============================================
# 请求头
# ============================================
headers = request.headers # 请求头字典
content_type = request.content_type # Content-Type头
content_length = request.content_length # Content-Length
mimetype = request.mimetype # MIME类型
mimetype_params = request.mimetype_params # MIME参数
# ============================================
# Cookie
# ============================================
cookies = request.cookies # Cookie字典
# ============================================
# 客户端信息
# ============================================
remote_addr = request.remote_addr # 客户端IP
user_agent = request.user_agent # User-Agent对象
referrer = request.referrer # 来源页面
origin = request.origin # Origin头
# ============================================
# 其他属性
# ============================================
environ = request.environ # WSGI环境字典
is_json = request.is_json # 是否为JSON请求
is_secure = request.is_secure # 是否为HTTPS
is_xhr = request.is_xhr # 是否为AJAX请求(已弃用)
blueprint = request.blueprint # 当前蓝图名
endpoint = request.endpoint # 当前端点名
view_args = request.view_args # URL参数字典
routing_exception = request.routing_exception # 路由异常
return {
'method': method,
'path': path,
'args': dict(args),
'is_json': is_json,
'remote_addr': remote_addr
}
2.2 请求参数获取详解
python
"""
请求参数获取详解
"""
from flask import Flask, request, jsonify
app = Flask(__name__)
# ============================================
# URL查询参数 (request.args)
# ============================================
@app.route('/search')
def search():
"""
URL查询参数示例
URL: /search?q=flask&page=2&sort=date
request.args 是 ImmutableMultiDict 类型
"""
# 获取单个值
query = request.args.get('q') # 'flask'
query = request.args.get('q', default='') # 带默认值
query = request.args['q'] # 不存在会抛出KeyError
# 获取多个同名参数
tags = request.args.getlist('tag') # ['python', 'web']
# 转换类型
page = request.args.get('page', default=1, type=int)
price = request.args.get('price', type=float)
debug = request.args.get('debug', type=bool, default=False)
# 获取所有参数
all_args = dict(request.args)
return jsonify({
'query': query,
'page': page,
'tags': tags,
'all_args': all_args
})
# ============================================
# 表单数据 (request.form)
# ============================================
@app.route('/login', methods=['POST'])
def login():
"""
表单数据示例
Content-Type: application/x-www-form-urlencoded
request.form 是 ImmutableMultiDict 类型
"""
username = request.form.get('username')
password = request.form.get('password')
remember = request.form.get('remember', type=bool, default=False)
# 获取所有表单字段
all_form = dict(request.form)
return jsonify({
'username': username,
'remember': remember
})
# ============================================
# JSON数据 (request.json)
# ============================================
@app.route('/api/users', methods=['POST'])
def create_user():
"""
JSON数据示例
Content-Type: application/json
request.json 自动解析JSON请求体
"""
# 方式1: 使用 request.json
data = request.json
# 方式2: 使用 request.get_json()
data = request.get_json()
data = request.get_json(force=True) # 强制解析(忽略Content-Type)
data = request.get_json(silent=True) # 解析失败返回None
data = request.get_json(cache=True) # 缓存解析结果
if data is None:
return jsonify({'error': 'Invalid JSON'}), 400
username = data.get('username')
email = data.get('email')
return jsonify({
'username': username,
'email': email
}), 201
# ============================================
# 合并参数 (request.values)
# ============================================
@app.route('/filter')
def filter_items():
"""
request.values 合并了 args 和 form
优先级: form > args
"""
# 可以同时获取URL参数和表单数据
category = request.values.get('category')
page = request.values.get('page', default=1, type=int)
return jsonify({
'category': category,
'page': page
})
# ============================================
# 原始请求体 (request.data)
# ============================================
@app.route('/raw', methods=['POST'])
def raw_data():
"""
原始请求体数据
当Content-Type不是表单或JSON时使用
"""
# 原始字节
raw_bytes = request.data
# 获取字符串
raw_text = request.data.decode('utf-8')
# 获取流
stream = request.stream
stream_data = stream.read()
return jsonify({
'length': len(raw_bytes),
'content_type': request.content_type
})
# ============================================
# 文件上传 (request.files)
# ============================================
@app.route('/upload', methods=['POST'])
def upload_file():
"""
文件上传示例
Content-Type: multipart/form-data
request.files 是 MultiDict 类型
"""
# 获取单个文件
file = request.files.get('file')
if file is None:
return jsonify({'error': 'No file uploaded'}), 400
# 文件属性
filename = file.filename # 原始文件名
content_type = file.content_type # 文件MIME类型
content_length = file.content_length # 文件大小
# 读取文件内容
content = file.read()
# 保存文件
# file.save('/path/to/save/' + filename)
# 安全文件名处理
from werkzeug.utils import secure_filename
safe_name = secure_filename(filename)
# 获取多个文件
files = request.files.getlist('files')
return jsonify({
'filename': filename,
'safe_name': safe_name,
'content_type': content_type,
'size': len(content)
})
2.3 请求头操作
python
"""
请求头操作详解
"""
from flask import Flask, request, jsonify
app = Flask(__name__)
@app.route('/headers')
def headers_info():
"""
请求头信息
request.headers 是 EnvironHeaders 类型
类似字典,但键不区分大小写
"""
# 获取单个请求头
content_type = request.headers.get('Content-Type')
user_agent = request.headers.get('User-Agent')
authorization = request.headers.get('Authorization')
# 不区分大小写
content_type = request.headers.get('content-type') # 同上
# 获取所有请求头
all_headers = dict(request.headers)
# 常用请求头快捷属性
host = request.host
origin = request.origin
referrer = request.referrer
# 判断请求头是否存在
has_auth = 'Authorization' in request.headers
return jsonify({
'content_type': content_type,
'user_agent': str(user_agent),
'has_auth': has_auth,
'all_headers': all_headers
})
@app.route('/auth-check')
def auth_check():
"""
Authorization头解析
"""
auth_header = request.headers.get('Authorization')
if auth_header is None:
return jsonify({'error': 'No authorization header'}), 401
# Bearer Token
if auth_header.startswith('Bearer '):
token = auth_header[7:]
return jsonify({'token': token})
# Basic Auth
if auth_header.startswith('Basic '):
import base64
credentials = base64.b64decode(auth_header[6:]).decode('utf-8')
username, password = credentials.split(':', 1)
return jsonify({'username': username})
return jsonify({'error': 'Invalid authorization header'}), 401
2.4 Cookie操作
python
"""
Cookie操作详解
"""
from flask import Flask, request, make_response
app = Flask(__name__)
@app.route('/get-cookies')
def get_cookies():
"""
获取Cookie
request.cookies 是字典类型
"""
# 获取单个Cookie
session_id = request.cookies.get('session_id')
user_pref = request.cookies.get('theme', default='light')
# 获取所有Cookie
all_cookies = dict(request.cookies)
return {
'session_id': session_id,
'theme': user_pref,
'all_cookies': all_cookies
}
@app.route('/set-cookies')
def set_cookies():
"""
设置Cookie
通过Response对象设置
"""
resp = make_response('Cookies set')
# 基本设置
resp.set_cookie('username', 'john')
# 完整参数
resp.set_cookie(
key='session_id', # Cookie名称
value='abc123', # Cookie值
max_age=3600, # 最大存活时间(秒)
expires=None, # 过期时间
path='/', # 有效路径
domain=None, # 有效域名
secure=True, # 仅HTTPS传输
httponly=True, # 禁止JS访问
samesite='Lax' # 同站策略: None, Lax, Strict
)
# 删除Cookie
resp.delete_cookie('old_cookie')
return resp
三、请求钩子
3.1 钩子函数详解
python
"""
Flask请求钩子详解
请求钩子允许在请求处理的不同阶段执行自定义代码
"""
from flask import Flask, request, g, jsonify
import time
import functools
app = Flask(__name__)
# ============================================
# before_request: 请求前执行
# ============================================
@app.before_request
def before_request_handler():
"""
每个请求前执行
返回值:
- None: 继续处理请求
- Response: 中断请求,直接返回响应
常用场景:
- 请求日志记录
- 用户认证检查
- 请求计时
- 数据库连接初始化
"""
# 请求计时
g.start_time = time.time()
# 请求日志
app.logger.info(
f'Request: {request.method} {request.path} '
f'from {request.remote_addr}'
)
# 示例: API请求认证检查
if request.path.startswith('/api/'):
token = request.headers.get('Authorization')
if not token:
return jsonify({'error': 'Unauthorized'}), 401
# ============================================
# before_first_request: 首次请求前执行(已弃用)
# ============================================
# Flask 2.3+ 已弃用,建议使用其他方式初始化
# 如应用工厂模式中的初始化代码
# ============================================
# after_request: 请求后执行
# ============================================
@app.after_request
def after_request_handler(response):
"""
每个请求后执行(视图函数返回响应后)
参数:
response: Response对象
返回值:
必须返回Response对象
特点:
- 即使视图函数抛出异常,也会执行
- 可以修改响应对象
常用场景:
- 添加响应头
- 响应日志记录
- 性能监控
"""
# 计算请求耗时
if hasattr(g, 'start_time'):
elapsed = time.time() - g.start_time
response.headers['X-Response-Time'] = f'{elapsed:.3f}s'
# 添加安全响应头
response.headers['X-Content-Type-Options'] = 'nosniff'
response.headers['X-Frame-Options'] = 'SAMEORIGIN'
# 响应日志
app.logger.info(
f'Response: {response.status_code} '
f'for {request.method} {request.path}'
)
return response
# ============================================
# teardown_request: 请求销毁时执行
# ============================================
@app.teardown_request
def teardown_request_handler(exception=None):
"""
请求上下文销毁时执行
参数:
exception: 异常对象(如果有异常)
特点:
- 无论是否发生异常都会执行
- 在after_request之后执行
- 不能返回值
常用场景:
- 数据库会话关闭
- 资源清理
- 错误日志记录
"""
# 数据库会话清理
# db.session.remove()
# 异常日志
if exception:
app.logger.error(f'Request error: {exception}')
# ============================================
# teardown_appcontext: 应用上下文销毁时执行
# ============================================
@app.teardown_appcontext
def teardown_appcontext_handler(exception=None):
"""
应用上下文销毁时执行
在teardown_request之后执行
通常用于清理应用级别的资源
"""
# 清理数据库连接池
pass
# ============================================
# 钩子执行顺序
# ============================================
"""
请求钩子执行顺序:
正常请求:
1. before_request
2. 视图函数
3. after_request
4. teardown_request
5. teardown_appcontext
异常请求:
1. before_request
2. 视图函数(抛出异常)
3. after_request(不执行)
4. teardown_request(执行,exception参数有值)
5. teardown_appcontext
"""
3.2 钩子函数应用示例
python
"""
请求钩子实际应用示例
"""
from flask import Flask, request, g, jsonify
import time
import logging
from functools import wraps
app = Flask(__name__)
# ============================================
# 示例1: 请求性能监控
# ============================================
@app.before_request
def start_timer():
"""开始计时"""
g.start_time = time.time()
@app.after_request
def end_timer(response):
"""结束计时并记录"""
if hasattr(g, 'start_time'):
elapsed = time.time() - g.start_time
# 添加响应头
response.headers['X-Response-Time'] = f'{elapsed:.4f}s'
# 慢请求告警
if elapsed > 1.0:
app.logger.warning(
f'Slow request: {request.method} {request.path} '
f'took {elapsed:.4f}s'
)
return response
# ============================================
# 示例2: 请求日志中间件
# ============================================
import json
@app.before_request
def log_request():
"""记录请求信息"""
g.request_id = request.headers.get('X-Request-ID', 'N/A')
log_data = {
'request_id': g.request_id,
'method': request.method,
'path': request.path,
'args': dict(request.args),
'remote_addr': request.remote_addr,
'user_agent': str(request.user_agent),
}
app.logger.info(f'Request: {json.dumps(log_data)}')
@app.after_request
def log_response(response):
"""记录响应信息"""
log_data = {
'request_id': getattr(g, 'request_id', 'N/A'),
'status_code': response.status_code,
'content_length': response.content_length,
}
app.logger.info(f'Response: {json.dumps(log_data)}')
return response
# ============================================
# 示例3: 数据库会话管理
# ============================================
@app.before_request
def init_db_session():
"""初始化数据库会话"""
# g.db_session = db.create_session()
pass
@app.teardown_request
def close_db_session(exception=None):
"""关闭数据库会话"""
session = getattr(g, 'db_session', None)
if session is not None:
if exception is not None:
session.rollback()
session.close()
# ============================================
# 示例4: API认证装饰器与钩子结合
# ============================================
def api_auth_required(f):
"""API认证装饰器"""
@wraps(f)
def decorated(*args, **kwargs):
token = request.headers.get('Authorization')
if not token or not token.startswith('Bearer '):
return jsonify({'error': 'Invalid token'}), 401
token = token[7:] # Remove 'Bearer ' prefix
# 验证token
user = verify_token(token)
if user is None:
return jsonify({'error': 'Invalid or expired token'}), 401
g.current_user = user
return f(*args, **kwargs)
return decorated
def verify_token(token):
"""验证Token(示例)"""
# 实际项目中使用JWT或其他方式验证
if token == 'valid-token':
return {'id': 1, 'username': 'admin'}
return None
@app.route('/api/protected')
@api_auth_required
def protected_api():
"""受保护的API"""
return jsonify({
'message': 'Protected content',
'user': g.current_user
})
# ============================================
# 示例5: 蓝图级钩子
# ============================================
from flask import Blueprint
api_bp = Blueprint('api', __name__, url_prefix='/api')
@api_bp.before_request
def api_before_request():
"""蓝图级before_request"""
# 仅对/api/*路由生效
if request.method != 'OPTIONS':
# CORS预检请求不需要认证
pass
@api_bp.after_request
def api_after_request(response):
"""蓝图级after_request"""
# 添加CORS头
response.headers['Access-Control-Allow-Origin'] = '*'
response.headers['Access-Control-Allow-Methods'] = 'GET, POST, PUT, DELETE'
response.headers['Access-Control-Allow-Headers'] = 'Content-Type, Authorization'
return response
app.register_blueprint(api_bp)
四、响应对象详解
4.1 Response对象构建
python
"""
Flask响应对象详解
视图函数可以返回多种类型的值,Flask会自动转换为Response对象
"""
from flask import Flask, make_response, Response, jsonify, redirect
app = Flask(__name__)
# ============================================
# 返回值类型
# ============================================
@app.route('/string')
def return_string():
"""
返回字符串
自动转换为:
- Response对象
- status_code: 200
- Content-Type: text/html
"""
return 'Hello World'
@app.route('/tuple-string')
def return_tuple_string():
"""
返回元组: (body, status)
"""
return 'Created', 201
@app.route('/tuple-headers')
def return_tuple_headers():
"""
返回元组: (body, headers)
"""
return 'OK', {'X-Custom-Header': 'value'}
@app.route('/tuple-full')
def return_tuple_full():
"""
返回元组: (body, status, headers)
"""
return 'Created', 201, {'Content-Type': 'application/json'}
@app.route('/response-object')
def return_response():
"""
返回Response对象
"""
resp = Response('Hello World')
resp.status_code = 200
resp.headers['X-Custom'] = 'value'
return resp
# ============================================
# make_response() 函数
# ============================================
@app.route('/make-response')
def use_make_response():
"""
make_response() 创建响应对象
参数可以是:
- 字符串
- 元组
- Response对象
"""
# 从字符串创建
resp = make_response('Hello World')
# 从元组创建
resp = make_response(('Created', 201))
# 设置属性
resp.status_code = 200
resp.headers['X-Custom-Header'] = 'value'
resp.mimetype = 'application/json'
# 设置Cookie
resp.set_cookie('username', 'john', max_age=3600)
return resp
# ============================================
# Response对象属性和方法
# ============================================
@app.route('/response-details')
def response_details():
"""Response对象详细示例"""
resp = make_response('Response Body')
# 响应体
resp.data = 'New Body' # 设置响应体(字节或字符串)
body = resp.get_data(as_text=True) # 获取响应体(字符串)
body_bytes = resp.get_data() # 获取响应体(字节)
# 状态
resp.status_code = 200 # 状态码
resp.status = '200 OK' # 状态字符串
# 响应头
resp.headers['X-Custom'] = 'value' # 设置响应头
content_type = resp.headers.get('Content-Type')
# MIME类型
resp.mimetype = 'application/json' # MIME类型
resp.content_type = 'application/json; charset=utf-8' # 完整Content-Type
# 内容长度
length = resp.content_length # 获取内容长度
# Cookie操作
resp.set_cookie(
key='session_id',
value='abc123',
max_age=3600,
expires=None,
path='/',
domain=None,
secure=True,
httponly=True,
samesite='Lax'
)
resp.delete_cookie('old_cookie')
# ETag
resp.set_etag('unique-etag')
# 缓存控制
resp.cache_control.max_age = 3600
resp.cache_control.no_cache = True
resp.cache_control.public = True
# 最后修改时间
from datetime import datetime
resp.last_modified = datetime.utcnow()
return resp
4.2 特殊响应类型
python
"""
特殊响应类型详解
"""
from flask import (
Flask, redirect, jsonify, send_file,
send_from_directory, make_response, Response
)
import io
import os
from datetime import datetime
app = Flask(__name__)
# ============================================
# redirect: 重定向
# ============================================
@app.route('/old-url')
def old_url():
"""
重定向到新URL
参数:
location: 目标URL
code: 状态码(默认302)
"""
# 基本重定向(302临时重定向)
return redirect('/new-url')
@app.route('/old-url-permanent')
def old_url_permanent():
"""永久重定向(301)"""
return redirect('/new-url', code=301)
@app.route('/old-url-307')
def old_url_307():
"""
307重定向:保留请求方法和请求体
适用场景:
- POST请求重定向
- 需要保留请求体
"""
return redirect('/new-url', code=307)
@app.route('/old-url-308')
def old_url_308():
"""308永久重定向:保留请求方法"""
return redirect('/new-url', code=308)
@app.route('/new-url')
def new_url():
return 'This is the new URL'
# 重定向状态码对比
"""
状态码对比:
- 301: 永久重定向,浏览器缓存,POST变GET
- 302: 临时重定向(默认),POST变GET
- 303: 临时重定向,强制GET
- 307: 临时重定向,保留请求方法和请求体
- 308: 永久重定向,保留请求方法和请求体
"""
# ============================================
# jsonify: JSON响应
# ============================================
@app.route('/api/user')
def api_user():
"""
JSON响应
自动设置:
- Content-Type: application/json
- 将Python对象序列化为JSON
"""
user = {
'id': 1,
'username': 'john',
'email': 'john@example.com',
'created_at': datetime.utcnow().isoformat()
}
return jsonify(user)
@app.route('/api/users')
def api_users():
"""返回列表"""
users = [
{'id': 1, 'name': 'John'},
{'id': 2, 'name': 'Jane'},
]
return jsonify(users)
@app.route('/api/status')
def api_status():
"""带状态码的JSON响应"""
return jsonify({'status': 'created'}), 201
@app.route('/api/error')
def api_error():
"""错误响应"""
return jsonify({'error': 'Not found'}), 404
# ============================================
# send_file: 文件响应
# ============================================
@app.route('/download/<filename>')
def download_file(filename):
"""
文件下载
参数:
path_or_file: 文件路径或文件对象
mimetype: MIME类型
as_attachment: 是否作为附件下载
download_name: 下载文件名
conditional: 是否支持条件请求
etag: ETag生成策略
last_modified: 最后修改时间
max_age: 缓存时间
"""
# 基本用法
return send_file(
f'/path/to/files/{filename}',
as_attachment=True
)
@app.route('/download/configured')
def download_configured():
"""完整参数示例"""
return send_file(
'/path/to/file.pdf',
mimetype='application/pdf',
as_attachment=True,
download_name='document.pdf',
conditional=True,
etag=True,
max_age=3600
)
@app.route('/download/memory')
def download_from_memory():
"""从内存下载"""
# 创建内存文件
data = b'Hello, this is file content from memory.'
file_obj = io.BytesIO(data)
return send_file(
file_obj,
mimetype='text/plain',
as_attachment=True,
download_name='memory-file.txt'
)
@app.route('/download/generated')
def download_generated():
"""生成CSV下载"""
import csv
# 创建内存文件
output = io.StringIO()
writer = csv.writer(output)
writer.writerow(['ID', 'Name', 'Email'])
writer.writerow([1, 'John', 'john@example.com'])
writer.writerow([2, 'Jane', 'jane@example.com'])
output.seek(0)
return send_file(
io.BytesIO(output.getvalue().encode('utf-8')),
mimetype='text/csv',
as_attachment=True,
download_name='users.csv'
)
# ============================================
# send_from_directory: 安全文件下载
# ============================================
@app.route('/files/<path:filename>')
def download_from_directory(filename):
"""
从目录安全下载文件
参数:
directory: 基础目录(绝对路径)
path: 相对路径
**kwargs: 其他参数传递给send_file
安全特性:
- 防止目录遍历攻击
- 自动处理路径规范化
"""
upload_dir = '/path/to/uploads'
return send_from_directory(
upload_dir,
filename,
as_attachment=True
)
@app.route('/images/<filename>')
def serve_image(filename):
"""服务图片文件"""
image_dir = os.path.join(app.root_path, 'static', 'images')
return send_from_directory(
image_dir,
filename,
mimetype='image/jpeg'
)
# ============================================
# 流式响应
# ============================================
@app.route('/stream')
def stream_response():
"""
流式响应
适用场景:
- 大文件下载
- 实时数据推送
- SSE (Server-Sent Events)
"""
def generate():
for i in range(100):
yield f'data: {i}\n\n'
import time
time.sleep(0.1)
return Response(
generate(),
mimetype='text/event-stream'
)
@app.route('/stream/csv')
def stream_csv():
"""流式CSV导出"""
import csv
def generate():
output = io.StringIO()
writer = csv.writer(output)
# 写入表头
writer.writerow(['ID', 'Name', 'Email'])
yield output.getvalue()
output.seek(0)
output.truncate(0)
# 写入数据
for i in range(1000):
writer.writerow([i, f'User {i}', f'user{i}@example.com'])
yield output.getvalue()
output.seek(0)
output.truncate(0)
return Response(
generate(),
mimetype='text/csv',
headers={
'Content-Disposition': 'attachment; filename=users.csv'
}
)
@app.route('/stream/large-file')
def stream_large_file():
"""大文件流式下载"""
def generate():
chunk_size = 8192 # 8KB chunks
with open('/path/to/large/file.bin', 'rb') as f:
while True:
chunk = f.read(chunk_size)
if not chunk:
break
yield chunk
return Response(
generate(),
mimetype='application/octet-stream',
headers={
'Content-Disposition': 'attachment; filename=large-file.bin'
}
)
4.3 Cookie完整操作
python
"""
Cookie完整操作详解
"""
from flask import Flask, make_response, request
app = Flask(__name__)
@app.route('/cookie/set')
def set_cookie():
"""
设置Cookie完整示例
"""
resp = make_response('Cookie set')
# 基本设置
resp.set_cookie('simple', 'value')
# 完整参数
resp.set_cookie(
key='session_id', # Cookie名称
value='abc123def456', # Cookie值
max_age=3600, # 最大存活时间(秒)
# 或使用expires指定过期时间
# expires=datetime(2024, 12, 31, 23, 59, 59),
path='/', # 有效路径
domain='.example.com', # 有效域名(带点表示包含子域名)
secure=True, # 仅HTTPS传输
httponly=True, # 禁止JavaScript访问
samesite='Lax' # 同站策略
)
# SameSite取值说明:
# - None: 允许跨站发送(需要Secure=True)
# - Lax: 允许顶级导航和GET表单跨站发送
# - Strict: 完全禁止跨站发送
return resp
@app.route('/cookie/get')
def get_cookie():
"""获取Cookie"""
# 获取单个Cookie
session_id = request.cookies.get('session_id')
# 带默认值
theme = request.cookies.get('theme', default='light')
# 获取所有Cookie
all_cookies = dict(request.cookies)
# 检查Cookie是否存在
has_session = 'session_id' in request.cookies
return {
'session_id': session_id,
'theme': theme,
'has_session': has_session
}
@app.route('/cookie/delete')
def delete_cookie():
"""删除Cookie"""
resp = make_response('Cookie deleted')
# 删除单个Cookie
resp.delete_cookie('session_id')
# 删除时需要匹配path和domain
resp.delete_cookie(
key='session_id',
path='/',
domain='.example.com'
)
return resp
@app.route('/cookie/update')
def update_cookie():
"""更新Cookie(重新设置即可)"""
resp = make_response('Cookie updated')
# 重新设置即更新
resp.set_cookie('theme', 'dark', max_age=3600)
return resp
五、错误处理机制
5.1 错误处理器注册
python
"""
Flask错误处理机制详解
"""
from flask import Flask, jsonify, render_template, request
app = Flask(__name__)
# ============================================
# HTTP错误处理器
# ============================================
@app.errorhandler(400)
def bad_request(error):
"""400 Bad Request"""
if request.is_json:
return jsonify({
'error': 'Bad Request',
'message': str(error.description)
}), 400
return render_template('errors/400.html', error=error), 400
@app.errorhandler(401)
def unauthorized(error):
"""401 Unauthorized"""
if request.is_json:
return jsonify({
'error': 'Unauthorized',
'message': 'Authentication required'
}), 401
return render_template('errors/401.html'), 401
@app.errorhandler(403)
def forbidden(error):
"""403 Forbidden"""
if request.is_json:
return jsonify({
'error': 'Forbidden',
'message': 'You do not have permission to access this resource'
}), 403
return render_template('errors/403.html'), 403
@app.errorhandler(404)
def not_found(error):
"""404 Not Found"""
if request.is_json:
return jsonify({
'error': 'Not Found',
'message': f'The requested URL {request.path} was not found'
}), 404
return render_template('errors/404.html'), 404
@app.errorhandler(405)
def method_not_allowed(error):
"""405 Method Not Allowed"""
if request.is_json:
return jsonify({
'error': 'Method Not Allowed',
'message': f'The method {request.method} is not allowed'
}), 405
return render_template('errors/405.html'), 405
@app.errorhandler(413)
def request_entity_too_large(error):
"""413 Request Entity Too Large"""
if request.is_json:
return jsonify({
'error': 'Request Entity Too Large',
'message': 'The uploaded file is too large'
}), 413
return render_template('errors/413.html'), 413
@app.errorhandler(422)
def unprocessable_entity(error):
"""422 Unprocessable Entity"""
if request.is_json:
return jsonify({
'error': 'Unprocessable Entity',
'message': 'Validation failed',
'details': getattr(error, 'details', {})
}), 422
return render_template('errors/422.html'), 422
@app.errorhandler(429)
def too_many_requests(error):
"""429 Too Many Requests"""
if request.is_json:
return jsonify({
'error': 'Too Many Requests',
'message': 'Rate limit exceeded'
}), 429
return render_template('errors/429.html'), 429
@app.errorhandler(500)
def internal_server_error(error):
"""500 Internal Server Error"""
# 记录错误日志
app.logger.error(f'Internal Server Error: {error}')
if request.is_json:
return jsonify({
'error': 'Internal Server Error',
'message': 'An unexpected error occurred'
}), 500
return render_template('errors/500.html'), 500
@app.errorhandler(502)
def bad_gateway(error):
"""502 Bad Gateway"""
if request.is_json:
return jsonify({
'error': 'Bad Gateway',
'message': 'Upstream server error'
}), 502
return render_template('errors/502.html'), 502
@app.errorhandler(503)
def service_unavailable(error):
"""503 Service Unavailable"""
if request.is_json:
return jsonify({
'error': 'Service Unavailable',
'message': 'Service temporarily unavailable'
}), 503
return render_template('errors/503.html'), 503
# ============================================
# 自定义异常处理器
# ============================================
class APIError(Exception):
"""API错误基类"""
def __init__(self, message, status_code=400, payload=None):
super().__init__()
self.message = message
self.status_code = status_code
self.payload = payload
def to_dict(self):
result = {
'error': self.__class__.__name__,
'message': self.message
}
if self.payload:
result['details'] = self.payload
return result
class ValidationError(APIError):
"""验证错误"""
def __init__(self, message, errors=None):
super().__init__(message, status_code=422, payload=errors)
class AuthenticationError(APIError):
"""认证错误"""
def __init__(self, message='Authentication failed'):
super().__init__(message, status_code=401)
class AuthorizationError(APIError):
"""授权错误"""
def __init__(self, message='Permission denied'):
super().__init__(message, status_code=403)
class ResourceNotFoundError(APIError):
"""资源不存在"""
def __init__(self, resource, resource_id):
super().__init__(
f'{resource} with id {resource_id} not found',
status_code=404
)
# 注册自定义异常处理器
@app.errorhandler(APIError)
def handle_api_error(error):
"""处理API错误"""
response = jsonify(error.to_dict())
response.status_code = error.status_code
return response
# 使用示例
@app.route('/api/users/<int:user_id>')
def get_user(user_id):
from myapp.models import User
user = User.query.get(user_id)
if user is None:
raise ResourceNotFoundError('User', user_id)
return jsonify(user.to_dict())
@app.route('/api/users', methods=['POST'])
def create_user():
data = request.get_json()
# 验证
errors = validate_user_data(data)
if errors:
raise ValidationError('Validation failed', errors)
# 创建用户
# ...
return jsonify({'id': 1}), 201
def validate_user_data(data):
"""验证用户数据"""
errors = {}
if not data.get('username'):
errors['username'] = 'Username is required'
elif len(data['username']) < 3:
errors['username'] = 'Username must be at least 3 characters'
if not data.get('email'):
errors['email'] = 'Email is required'
return errors
5.2 abort函数
python
"""
abort函数详解
用于主动中断请求并返回错误响应
"""
from flask import Flask, abort, jsonify, request
app = Flask(__name__)
@app.route('/admin')
def admin():
"""权限检查示例"""
if not is_admin():
abort(403) # 返回403 Forbidden
return 'Admin Panel'
@app.route('/user/<int:user_id>')
def get_user(user_id):
"""资源检查示例"""
user = get_user_from_db(user_id)
if user is None:
abort(404) # 返回404 Not Found
return jsonify(user)
@app.route('/api/data')
def api_data():
"""带描述的abort"""
if not request.args.get('key'):
abort(400, description='API key is required')
return jsonify({'data': 'value'})
# 自定义错误响应
@app.errorhandler(400)
def bad_request(error):
return jsonify({
'error': 'Bad Request',
'message': error.description
}), 400
# abort的HTTP状态码
"""
常用abort状态码:
- 400: Bad Request - 请求格式错误
- 401: Unauthorized - 未认证
- 403: Forbidden - 无权限
- 404: Not Found - 资源不存在
- 405: Method Not Allowed - 方法不允许
- 406: Not Acceptable - 无法接受
- 409: Conflict - 资源冲突
- 410: Gone - 资源已删除
- 413: Request Entity Too Large - 请求体过大
- 415: Unsupported Media Type - 不支持的媒体类型
- 422: Unprocessable Entity - 验证失败
- 429: Too Many Requests - 请求过多
- 500: Internal Server Error - 服务器错误
- 501: Not Implemented - 未实现
- 503: Service Unavailable - 服务不可用
"""
def is_admin():
"""检查是否为管理员"""
return False
def get_user_from_db(user_id):
"""从数据库获取用户"""
return None
5.3 错误处理最佳实践
python
"""
错误处理最佳实践
"""
from flask import Flask, jsonify, request, render_template
from functools import wraps
import traceback
app = Flask(__name__)
# ============================================
# 1. 统一错误响应格式
# ============================================
def error_response(message, status_code, details=None):
"""
统一错误响应格式
参数:
message: 错误消息
status_code: HTTP状态码
details: 详细信息
"""
response = {
'success': False,
'error': {
'message': message,
'status_code': status_code
}
}
if details:
response['error']['details'] = details
if app.debug:
response['error']['trace'] = traceback.format_exc()
return jsonify(response), status_code
# ============================================
# 2. 全局异常捕获
# ============================================
@app.errorhandler(Exception)
def handle_exception(error):
"""
捕获所有未处理的异常
生产环境不应暴露详细错误信息
"""
# 记录错误日志
app.logger.error(
f'Unhandled exception: {str(error)}\n'
f'Traceback: {traceback.format_exc()}'
)
# API请求返回JSON
if request.path.startswith('/api/'):
return error_response(
message='Internal server error',
status_code=500
)
# Web请求返回错误页面
return render_template('errors/500.html'), 500
# ============================================
# 3. 错误处理装饰器
# ============================================
def handle_errors(f):
"""
错误处理装饰器
捕获视图函数中的异常并返回统一格式
"""
@wraps(f)
def decorated(*args, **kwargs):
try:
return f(*args, **kwargs)
except APIError as e:
return error_response(
message=e.message,
status_code=e.status_code,
details=e.payload
)
except Exception as e:
app.logger.error(f'Error in {f.__name__}: {str(e)}')
return error_response(
message='Internal server error',
status_code=500
)
return decorated
@app.route('/api/protected')
@handle_errors
def protected_route():
"""使用错误处理装饰器"""
# 业务逻辑
if not validate_request():
raise ValidationError('Invalid request')
return jsonify({'data': 'success'})
# ============================================
# 4. 错误日志记录
# ============================================
import logging
from logging.handlers import RotatingFileHandler
import os
def setup_error_logging(app):
"""配置错误日志"""
if not app.debug:
# 确保日志目录存在
if not os.path.exists('logs'):
os.mkdir('logs')
# 文件日志
file_handler = RotatingFileHandler(
'logs/errors.log',
maxBytes=10 * 1024 * 1024, # 10MB
backupCount=5
)
file_handler.setFormatter(logging.Formatter(
'%(asctime)s %(levelname)s: %(message)s '
'[in %(pathname)s:%(lineno)d]'
))
file_handler.setLevel(logging.ERROR)
app.logger.addHandler(file_handler)
# 邮件通知(严重错误)
from logging.handlers import SMTPHandler
mail_handler = SMTPHandler(
mailhost=(app.config['MAIL_SERVER'], app.config['MAIL_PORT']),
fromaddr=app.config['MAIL_DEFAULT_SENDER'],
toaddrs=app.config['ADMINS'],
subject='[MyApp] Application Error',
credentials=(app.config['MAIL_USERNAME'], app.config['MAIL_PASSWORD'])
)
mail_handler.setLevel(logging.ERROR)
app.logger.addHandler(mail_handler)
# ============================================
# 5. 错误页面模板
# ============================================
"""
templates/errors/404.html:
<!DOCTYPE html>
<html>
<head>
<title>404 - Page Not Found</title>
</head>
<body>
<h1>404 - Page Not Found</h1>
<p>The page you are looking for does not exist.</p>
<a href="{{ url_for('main.index') }}">Go to Home</a>
</body>
</html>
templates/errors/500.html:
<!DOCTYPE html>
<html>
<head>
<title>500 - Internal Server Error</title>
</head>
<body>
<h1>500 - Internal Server Error</h1>
<p>Something went wrong. Please try again later.</p>
<a href="{{ url_for('main.index') }}">Go to Home</a>
</body>
</html>
"""
def validate_request():
return False
六、上下文管理机制
6.1 上下文栈结构
python
"""
Flask上下文管理详解
Flask使用两个上下文栈来管理请求状态:
1. _request_ctx_stack: 请求上下文栈
2. _app_ctx_stack: 应用上下文栈
"""
from flask import Flask, request, current_app, g
app = Flask(__name__)
# ============================================
# 请求上下文
# ============================================
"""
请求上下文包含:
- request: 当前请求对象
- session: 当前会话对象
生命周期: 一个HTTP请求
"""
@app.route('/request-context')
def request_context_demo():
"""
请求上下文示例
"""
# request对象在请求上下文中可用
method = request.method
path = request.path
# session对象也在请求上下文中
# session['key'] = 'value'
return {
'method': method,
'path': path
}
# ============================================
# 应用上下文
# ============================================
"""
应用上下文包含:
- current_app: 当前Flask应用实例
- g: 请求期间存储数据的命名空间
生命周期: 一个请求或CLI命令
"""
@app.route('/app-context')
def app_context_demo():
"""
应用上下文示例
"""
# current_app指向当前Flask应用
debug = current_app.debug
config = current_app.config
# g是请求级别的存储空间
g.user_id = 123
g.start_time = time.time()
return {
'debug': debug,
'user_id': g.user_id
}
# ============================================
# 手动创建上下文
# ============================================
import time
def background_task():
"""
后台任务需要手动创建应用上下文
"""
with app.app_context():
# 现在可以访问current_app和g
print(f'App name: {current_app.name}')
# 可以访问数据库等扩展
# from myapp.extensions import db
# users = db.session.query(User).all()
def test_request_context():
"""
测试请求上下文
"""
with app.test_request_context('/test?arg=value'):
# 现在可以访问request
print(f'Path: {request.path}')
print(f'Args: {request.args}')
# 可以调用视图函数
# response = app.view_functions['endpoint']()
# ============================================
# g对象详解
# ============================================
@app.before_request
def before_request():
"""
在before_request中设置g对象
"""
g.start_time = time.time()
g.request_id = request.headers.get('X-Request-ID')
@app.route('/g-demo')
def g_demo():
"""
g对象使用示例
"""
# 获取before_request中设置的值
start_time = g.get('start_time')
request_id = g.get('request_id')
# 设置新值
g.processed = True
return {
'request_id': request_id,
'elapsed': time.time() - start_time if start_time else None
}
@app.teardown_request
def teardown_request(exception=None):
"""
在teardown_request中清理g对象
"""
# g对象会自动清理,无需手动操作
pass
6.2 上下文生命周期
View AppContext RequestContext Flask WSGI Client View AppContext RequestContext Flask WSGI Client alt [AppContext not exists] alt [Last request] HTTP Request call(environ) Create RequestContext push() to _request_ctx_stack Check AppContext exists? Create AppContext push() to _app_ctx_stack before_request hooks Call view function Response after_request hooks teardown_request hooks pop() Check last request? teardown_appcontext hooks pop() Response HTTP Response
6.3 上下文代理原理
python
"""
Flask上下文代理原理
request, current_app, g, session 都是代理对象
它们通过LocalProxy实现对栈顶对象的透明访问
"""
from werkzeug.local import LocalProxy, LocalStack
# Flask内部的实现(简化版)
# 创建栈
_request_ctx_stack = LocalStack()
_app_ctx_stack = LocalStack()
# 创建代理
def _get_request():
"""获取当前请求对象"""
top = _request_ctx_stack.top
if top is None:
raise RuntimeError('Working outside of request context')
return top.request
request = LocalProxy(_get_request)
def _get_current_app():
"""获取当前应用对象"""
top = _app_ctx_stack.top
if top is None:
raise RuntimeError('Working outside of application context')
return top.app
current_app = LocalProxy(_get_current_app)
# 使用示例
"""
代理对象的好处:
1. 无需显式传递request对象
2. 线程安全
3. 代码简洁
# 不使用代理(需要显式传递)
def view_function(request):
return request.method
# 使用代理(自动获取当前请求)
def view_function():
return request.method
"""
七、WSGI中间件
7.1 中间件模式
python
"""
WSGI中间件模式详解
中间件是包裹Flask应用的WSGI应用
可以在请求到达Flask前后进行处理
"""
from flask import Flask
app = Flask(__name__)
# ============================================
# 中间件基本结构
# ============================================
class SimpleMiddleware:
"""
简单中间件示例
WSGI中间件必须:
1. 接受一个WSGI应用作为参数
2. 实现__call__方法
3. 调用被包裹的应用
"""
def __init__(self, app):
self.app = app
def __call__(self, environ, start_response):
"""
处理请求
参数:
environ: WSGI环境字典
start_response: 开始响应的回调函数
"""
# 请求前处理
print(f'Before request: {environ["PATH_INFO"]}')
# 调用被包裹的应用
def custom_start_response(status, headers, exc_info=None):
# 可以修改响应头
headers.append(('X-Middleware', 'SimpleMiddleware'))
return start_response(status, headers, exc_info)
# 调用下一个应用
response = self.app(environ, custom_start_response)
# 响应后处理
print(f'After response: {status}')
return response
# 应用中间件
app.wsgi_app = SimpleMiddleware(app.wsgi_app)
# ============================================
# ProxyFix中间件
# ============================================
from werkzeug.middleware.proxy_fix import ProxyFix
"""
ProxyFix中间件
用于反向代理环境,修复请求头中的真实IP等信息
参数:
x_for: X-Forwarded-For头的信任层数
x_proto: X-Forwarded-Proto头的信任层数
x_host: X-Forwarded-Host头的信任层数
x_port: X-Forwarded-Port头的信任层数
x_prefix: X-Forwarded-Prefix头的信任层数
"""
# 基本配置
app.wsgi_app = ProxyFix(
app.wsgi_app,
x_for=1, # 信任一层代理
x_proto=1,
x_host=1,
x_port=1
)
# 多层代理配置
app.wsgi_app = ProxyFix(
app.wsgi_app,
x_for=2, # 信任两层代理
x_proto=1,
x_host=1
)
# ============================================
# 性能分析中间件
# ============================================
from werkzeug.middleware.profiler import ProfilerMiddleware
"""
ProfilerMiddleware
用于性能分析,生成性能报告
"""
# 启用性能分析
app.wsgi_app = ProfilerMiddleware(
app.wsgi_app,
profile_dir='profiles' # 性能报告输出目录
)
# ============================================
# 自定义请求计时中间件
# ============================================
import time
class TimingMiddleware:
"""请求计时中间件"""
def __init__(self, app):
self.app = app
def __call__(self, environ, start_response):
start_time = time.time()
def custom_start_response(status, headers, exc_info=None):
elapsed = time.time() - start_time
headers.append(('X-Response-Time', f'{elapsed:.4f}s'))
return start_response(status, headers, exc_info)
return self.app(environ, custom_start_response)
# ============================================
# 自定义错误处理中间件
# ============================================
class ErrorHandlingMiddleware:
"""全局错误处理中间件"""
def __init__(self, app):
self.app = app
def __call__(self, environ, start_response):
try:
return self.app(environ, start_response)
except Exception as e:
# 记录错误
import traceback
error_trace = traceback.format_exc()
print(f'Error: {error_trace}')
# 返回错误响应
status = '500 Internal Server Error'
headers = [('Content-Type', 'application/json')]
start_response(status, headers)
import json
return [json.dumps({
'error': 'Internal Server Error',
'message': str(e) if environ.get('DEBUG') else 'Server error'
}).encode('utf-8')]
# ============================================
# 静态文件中间件
# ============================================
from whitenoise import WhiteNoise
"""
WhiteNoise中间件
用于高效服务静态文件,支持缓存和压缩
"""
# 应用WhiteNoise
app.wsgi_app = WhiteNoise(
app.wsgi_app,
root='static/', # 静态文件目录
prefix='static/', # URL前缀
max_age=31536000 # 缓存时间(1年)
)
# 添加额外的静态文件目录
# app.wsgi_app.add_files('media/', prefix='media/')
八、异步视图支持
8.1 异步视图基础
python
"""
Flask异步视图支持(Flask 2.0+)
使用async def定义异步视图函数
"""
from flask import Flask, jsonify
import asyncio
import aiohttp
app = Flask(__name__)
# ============================================
# 基本异步视图
# ============================================
@app.route('/async-hello')
async def async_hello():
"""基本异步视图"""
await asyncio.sleep(0.1) # 模拟异步操作
return 'Hello, Async World!'
# ============================================
# 异步HTTP请求
# ============================================
@app.route('/fetch-data')
async def fetch_data():
"""异步获取外部数据"""
async with aiohttp.ClientSession() as session:
async with session.get('https://api.example.com/data') as response:
data = await response.json()
return jsonify(data)
# ============================================
# 并发异步操作
# ============================================
@app.route('/parallel-fetch')
async def parallel_fetch():
"""并发获取多个数据源"""
async def fetch_url(url):
async with aiohttp.ClientSession() as session:
async with session.get(url) as response:
return await response.json()
# 并发执行多个请求
urls = [
'https://api.example.com/users',
'https://api.example.com/posts',
'https://api.example.com/comments'
]
results = await asyncio.gather(*[fetch_url(url) for url in urls])
return jsonify({
'users': results[0],
'posts': results[1],
'comments': results[2]
})
# ============================================
# 异步数据库操作
# ============================================
@app.route('/async-users')
async def async_users():
"""
异步数据库查询
需要使用异步数据库驱动如:
- SQLAlchemy 2.0+ async
- asyncpg (PostgreSQL)
- aiomysql (MySQL)
"""
# 使用SQLAlchemy 2.0异步示例
# from sqlalchemy.ext.asyncio import AsyncSession
# async with AsyncSession(engine) as session:
# result = await session.execute(select(User))
# users = result.scalars().all()
return jsonify({'users': []})
# ============================================
# 异步请求钩子
# ============================================
@app.before_request
async def async_before_request():
"""异步before_request钩子"""
# 可以执行异步操作
# await some_async_operation()
pass
@app.after_request
async def async_after_request(response):
"""异步after_request钩子"""
# 可以执行异步操作
return response
# ============================================
# 异步与同步混用
# ============================================
@app.route('/mixed')
def mixed_view():
"""
同步视图中调用异步函数
使用asyncio.run()或loop
"""
async def async_operation():
await asyncio.sleep(0.1)
return 'async result'
# 在同步上下文中运行异步函数
result = asyncio.run(async_operation())
return result
九、最佳实践总结
python
"""
Flask请求处理最佳实践
"""
# ============================================
# 1. 请求处理
# ============================================
"""
✅ 使用request对象获取请求数据
✅ 使用适当的参数获取方法
✅ 验证所有输入数据
✅ 处理文件上传时使用secure_filename
✅ 使用g对象存储请求级别数据
"""
# ============================================
# 2. 响应处理
# ============================================
"""
✅ API返回JSON格式响应
✅ 使用正确的HTTP状态码
✅ 设置适当的安全响应头
✅ 大文件使用流式响应
✅ 使用make_response构建复杂响应
"""
# ============================================
# 3. 错误处理
# ============================================
"""
✅ 注册全局错误处理器
✅ 使用自定义异常类
✅ 统一错误响应格式
✅ 记录错误日志
✅ 生产环境不暴露详细错误信息
"""
# ============================================
# 4. 上下文管理
# ============================================
"""
✅ 理解请求上下文和应用上下文
✅ 后台任务使用app_context
✅ 测试使用test_request_context
✅ 避免在全局作用域访问上下文代理
"""
# ============================================
# 5. 性能优化
# ============================================
"""
✅ 使用请求钩子进行预处理
✅ 使用缓存减少重复计算
✅ 异步处理耗时操作
✅ 合理使用中间件
✅ 监控请求处理时间
"""