1. 为什么值得花时间啃DRF的请求响应源码很多人用Django REST Framework后面统一简称DRF写接口写个APIView、挂个api_view装饰器、配个Serializer接口就跑起来了。日常业务开发这么用完全没问题但一旦遇到下面这些场景不读源码就会卡住想在请求进来之前统一做一层参数预处理但不知道request.data到底在哪一步被解析出来改来改去总是拿不到想要的数据自定义了一个认证类返回的request.user有时候是匿名用户有时候又是真实用户搞不清DRF在哪个环节把用户对象塞进去的想给所有响应统一加一个业务状态码包装结果发现有的接口走了自定义Response有的走了异常处理返回结构对不上面试或者技术评审时被问到DRF的Request和Django原生的HttpRequest到底差在哪只能含糊地说封装了一下。这些问题的答案全都在DRF的请求响应组件源码里。这篇文章就是带你把这块源码从头到尾捋一遍不是泛泛地讲概念而是把关键类、关键方法、关键调用链拆开让你看完之后能自己动手改、动手扩展。先明确一下这篇文章适合谁有Django基础、用过DRF写过接口、但没系统读过DRF源码的开发者。如果你连View和APIView的区别都还不清楚建议先把官方教程过一遍再回来。另外这篇文章基于DRF 3.x的稳定版本不同小版本之间细节可能有差异但核心设计思想是一致的。读源码这件事我的建议是不要一上来就从头读到尾。DRF的源码文件不算多但类之间的继承和组合关系比较绕。正确的姿势是从一个请求的生命周期入手顺着调用链往下追。一个HTTP请求打到DRF的接口上大致会经历这几个阶段WSGI服务器把请求交给Django的WSGIHandlerDjango根据URL配置找到对应的视图函数或视图类如果是DRF的APIView会走dispatch方法dispatch里依次执行认证、权限、限流、解析请求数据、调用处理方法、渲染响应响应经过内容协商和渲染器处理后返回给客户端。这条链路里请求响应组件主要涉及request.py、response.py、parsers.py、renderers.py、negotiation.py这几个文件。下面我按这条链路把每个环节的核心逻辑拆开讲。2. DRF的Request到底对HttpRequest做了什么封装2.1 从APIView.dispatch看Request的诞生要理解DRF的Request得先找到它是在哪里被创建的。答案在APIView的dispatch方法里源码大致是这样的我省略了部分细节def dispatch(self, request, *args, **kwargs): self.args args self.kwargs kwargs request self.initialize_request(request, *args, **kwargs) self.request request self.headers self.default_response_headers try: self.initial(request, *args, **kwargs) if request.method.lower() in self.http_method_names: handler getattr(self, request.method.lower(), self.http_method_not_allowed) else: handler self.http_method_not_allowed response handler(request, *args, **kwargs) except Exception as exc: response self.handle_exception(exc) self.response self.finalize_response(request, response, *args, **kwargs) return self.response关键就是initialize_request这一步。它把Django原生的HttpRequest对象包了一层生成了DRF自己的Request对象。这个Request对象在后续整个处理流程里就是self.request也是你视图方法里拿到的那个request。initialize_request的实现def initialize_request(self, request, *args, **kwargs): parser_context self.get_parser_context(request) return Request( request, parsersself.get_parsers(), authenticatorsself.get_authenticators(), negotiatorself.get_content_negotiator(), parser_contextparser_context )可以看到DRF的Request在创建时就被注入了四样东西解析器列表、认证器列表、内容协商器、解析上下文。这个设计很关键——它把请求数据怎么解析用户身份怎么认证响应格式怎么协商这些策略性的东西从请求对象本身解耦出来通过组合的方式注入。这也是为什么你可以在全局配置里换解析器、换认证类而不用改Request的代码。2.2 Request的__getattr__魔法为什么request.user能直接用DRF的Request类里有一个非常巧妙的设计就是__getattr__方法def __getattr__(self, attr): try: return getattr(self._request, attr) except AttributeError: return self.__getattribute__(attr)这段代码的意思是当你访问Request对象上某个属性而它自己没定义时会去底层的_request也就是原生HttpRequest上找。所以request.method、request.path、request.META这些属性其实都是直接透传到底层HttpRequest的。但request.user是个例外。Request类里明确定义了user属性property def user(self): if not hasattr(self, _user): with wrap_attributeerrors(): self._authenticate() return self._user第一次访问request.user时会触发_authenticate()也就是执行认证流程。认证完成后把结果缓存到_user后续再访问就直接返回缓存。这个懒加载设计很聪明——如果某个接口不需要用户身份就不会触发认证省掉一次数据库查询或token校验。_authenticate的核心逻辑def _authenticate(self): for authenticator in self.authenticators: try: user_auth_tuple authenticator.authenticate(self) except exceptions.APIException: self._not_authenticated() raise if user_auth_tuple is not None: self._authenticator authenticator self.user, self.auth user_auth_tuple return self._not_authenticated()它会遍历所有配置的认证器谁先返回了(user, auth)元组就用谁的结果。如果所有认证器都返回None就调用_not_authenticated()把user设为AnonymousUserauth设为None。这里有个容易踩的坑认证器返回None和抛出异常是两种不同的语义。返回None表示这个认证器不处理这个请求交给下一个抛出APIException表示这个请求认证失败了直接中断。很多人在自定义认证类时认证失败直接返回None结果请求继续往下走最后以匿名用户身份通过了权限检查造成越权。正确的做法是认证失败时抛AuthenticationFailed。2.3 request.data的解析时机与坑request.data是DRF里用得最多的属性之一它的实现property def data(self): if not _hasattr(self, _full_data): self._load_data_and_files() return self._full_data同样是懒加载。第一次访问时调用_load_data_and_files()里面会调用_parse()_parse()再遍历self.parsers找到第一个能处理当前Content-Type的解析器用它把请求体解析成Python对象。这里有几个实战中经常遇到的问题第一个坑request.data只能读一次。因为请求体是流式的解析器读完就没了。如果你在中间件或者某个装饰器里先读了一次request.data视图里再读就会报错或者拿到空数据。解决办法是在中间件里用request.bodyDjango原生属性会缓存或者把解析结果存起来复用。第二个坑request.data和request.POST不通用。对于application/json的请求request.POST是空的因为Django原生只解析表单数据。所以处理JSON请求必须用request.data。第三个坑文件上传时request.data包含文件对象。对于multipart/form-data请求request.data返回的是一个包含QueryDict和文件对象的合并字典。如果你直接把它序列化文件对象会报错。正确做法是用request.data取普通字段用request.FILES取文件。第四个坑解析器顺序影响性能。_parse()是遍历解析器列表找到第一个匹配的就用。如果你把JSONParser放在列表最后每个JSON请求都要先试过前面几个解析器。虽然试错成本不高但高频接口下还是有影响。建议把最常用的解析器放前面。2.4 认证、权限、限流在dispatch中的执行顺序回到dispatchinitialize_request之后是initial方法def initial(self, request, *args, **kwargs): self.format_kwarg self.get_format_suffix(**kwargs) neg self.perform_content_negotiation(request) request.accepted_renderer, request.accepted_media_type neg version, scheme self.determine_version(request, *args, **kwargs) request.version, request.versioning_scheme version, scheme self.perform_authentication(request) self.check_permissions(request) self.check_throttles(request)注意这个顺序内容协商 → 版本确定 → 认证 → 权限 → 限流。这个顺序不是随便排的背后有逻辑内容协商放最前面是因为后续的异常响应也需要知道用什么渲染器提前确定好可以避免异常处理时再协商一次认证在权限之前因为权限检查依赖request.user和request.auth限流放最后是因为限流可能依赖用户身份比如按用户限流所以要在认证之后。这里有个细节值得注意perform_authentication里其实只是访问了一下request.user触发懒加载认证。真正的认证逻辑在Request._authenticate里。这种触发式设计让认证和权限的边界很清晰。3. Response与渲染器一个响应是怎么被加工出来的3.1 Response类比HttpResponse多了什么DRF的Response继承自Django的SimpleTemplateResponse但加了不少东西。它的构造函数def __init__(self, dataNone, statusNone, template_nameNone, headersNone, exceptionFalse, content_typeNone): super().__init__(None, statusstatus) ... self.data data self.template_name template_name self.exception exception self.content_type content_type ...关键点在于Response初始化时并不立即渲染内容而是把未渲染的data存起来等到finalize_response阶段才根据协商好的渲染器去渲染。这就是所谓的延迟渲染。为什么要延迟渲染因为同一个data根据客户端Accept头的不同可能要渲染成JSON、XML或者 browsable API的HTML。如果在创建Response时就渲染就没法根据请求动态选择了。Response还重写了rendered_content属性property def rendered_content(self): renderer getattr(self, accepted_renderer, None) accepted_media_type getattr(self, accepted_media_type, None) context getattr(self, renderer_context, None) ... content renderer.render(self.data, accepted_media_type, context) ... return content这里会调用accepted_renderer.render()把data渲染成字节流。accepted_renderer是在finalize_response里设置的。3.2 finalize_response做了什么finalize_response是dispatch的最后一步def finalize_response(self, request, response, *args, **kwargs): if isinstance(response, Response): if not getattr(request, accepted_renderer, None): neg self.perform_content_negotiation(request, forceTrue) request.accepted_renderer, request.accepted_media_type neg response.accepted_renderer request.accepted_renderer response.accepted_media_type request.accepted_media_type response.renderer_context self.get_renderer_context() ... return response它做了一件重要的事把请求阶段协商好的渲染器和媒体类型绑定到响应对象上。这样响应在最终被Django处理时就知道该用什么渲染器渲染。get_renderer_context返回的上下文包含request、response、view、format、args、kwargs这些会传给渲染器的render方法。如果你自定义渲染器可以从这个上下文里拿到视图和请求信息。3.3 内容协商客户端要什么格式就给什么格式内容协商的核心在DefaultContentNegotiation类里主要方法是select_rendererdef select_renderer(self, request, renderers, format_suffixNone): format_query_param self.settings.URL_FORMAT_OVERRIDE format format_suffix or request.query_params.get(format_query_param) if format: renderers self.filter_renderers(renderers, format) accepts self.get_accept_list(request) for media_type_set in accepts: for renderer in renderers: if self.filter_renderer(renderer, media_type_set): return renderer, media_type_set ...协商的优先级是URL后缀 查询参数 Accept头。比如/api/users.json会优先匹配JSON渲染器/api/users?formatjson次之最后才看Accept头。这个优先级设计有实际意义浏览器默认的Accept头包含text/html如果只看Accept头浏览器访问接口会返回 browsable API的HTML页面。而通过URL后缀或查询参数可以强制指定返回JSON方便调试和前端调用。filter_renderers和filter_renderer的逻辑是先按format过滤出候选渲染器再按media_type匹配。匹配时会处理*/*、application/*这种通配符。3.4 自定义渲染器的正确姿势实际项目里经常需要自定义渲染器。比如统一响应格式class UnifiedJSONRenderer(JSONRenderer): def render(self, data, accepted_media_typeNone, renderer_contextNone): response renderer_context.get(response) if response and response.exception: wrapped {code: response.status_code, msg: str(data), data: None} else: wrapped {code: 0, msg: success, data: data} return super().render(wrapped, accepted_media_type, renderer_context)这里有个关键点异常响应和正常响应的data结构不一样。异常时data是错误详情正常时是业务数据。所以要根据response.exception区分处理。另一个坑是渲染器里不要做耗时操作。渲染器会被每个响应调用如果里面查数据库、调外部接口会严重拖慢响应。渲染器只应该做数据格式转换。还有一点render方法必须返回字节串不能返回字符串。JSONRenderer内部会做编码自定义渲染器如果直接返回字符串Django处理时会报错。4. 解析器与认证器的协作机制4.1 解析器如何匹配Content-Type解析器的基类BaseParser定义了media_type属性JSONParser的media_type是application/json。Request._parse()里的匹配逻辑def _parse(self): media_type self.content_type ... for parser in self.parsers: if parser.media_type media_type: ... return parser.parse(stream, media_type, self.parser_context) ...匹配是精确匹配media_type。但实际请求的Content-Type可能带参数比如application/json; charsetutf-8。DRF在content_type属性里做了处理会去掉参数部分property def content_type(self): meta self._request.META return meta.get(CONTENT_TYPE, meta.get(HTTP_CONTENT_TYPE, ))注意这里其实没有去掉参数而是在_parse里用media_type匹配时media_type是从content_type里取的。实际上DRF的匹配逻辑在parser.media_type media_type这一步如果content_type带参数会匹配不上。所以DRF在_parse里其实做了处理用media_type参数传入的是去掉参数后的类型。这个细节在不同版本里实现略有差异但核心思想是解析器匹配的是媒体类型的主类型不带参数。如果你自定义解析器media_type要写主类型比如application/xml不要写application/xml; charsetutf-8。4.2 认证器的authenticate返回值语义认证器的核心方法是authenticate(self, request)返回值有三种情况返回值含义后续行为(user, auth)元组认证成功设置request.user和request.auth停止遍历None不处理此请求继续下一个认证器抛出APIException认证失败中断返回401或403这个语义设计很关键。比如你有两个认证器TokenAuthentication和SessionAuthentication。API请求带token走第一个认证器成功浏览器请求带session第一个认证器返回None走第二个成功。这样就能同时支持两种认证方式。但要注意如果所有认证器都返回None请求会以匿名用户身份继续。所以如果你的接口需要登录必须在权限类里检查request.user.is_authenticated不能只依赖认证器。4.3 认证与权限的边界一个真实踩坑案例我之前遇到过一个越权问题某个接口配置了TokenAuthentication权限类是IsAuthenticated。测试时发现不带token也能访问成功。排查后发现认证器在token无效时返回了None而不是抛异常导致请求以匿名用户身份继续而权限类IsAuthenticated检查的是request.user.is_authenticated匿名用户应该是False为什么还通过了进一步排查发现权限类写成了class IsAuthenticated(BasePermission): def has_permission(self, request, view): return request.userrequest.user是匿名用户对象它是个真值truthy所以返回了True。正确的写法是return bool(request.user and request.user.is_authenticated)。这个坑的根源是认证器返回None的语义被误用了。认证器返回None应该是我不处理这个请求而不是认证失败。认证失败必须抛AuthenticationFailed。同时权限类要严格检查is_authenticated不能只判断对象是否存在。5. 从源码看扩展点哪些地方可以安全地动手5.1 自定义Request的三种方式有时候需要在请求对象上加一些自定义属性或方法。有三种方式方式一继承Request重写initialize_request。class CustomRequest(Request): property def tenant(self): return self.META.get(HTTP_X_TENANT_ID) class CustomAPIView(APIView): def initialize_request(self, request, *args, **kwargs): return CustomRequest( request, parsersself.get_parsers(), authenticatorsself.get_authenticators(), negotiatorself.get_content_negotiator(), parser_contextself.get_parser_context(request) )这种方式最干净但需要所有视图都继承CustomAPIView。方式二用中间件给request打补丁。在中间件里给request对象动态加属性。但要注意中间件拿到的还是原生HttpRequestDRF的Request是在视图的initialize_request里才创建的。所以中间件里加属性要确保属性名不冲突且DRF的Request.__getattr__能透传过去。方式三用parser_context传额外数据。get_parser_context返回的字典会传给解析器你可以在视图里重写这个方法加入自定义数据。但这种方式只影响解析器不影响请求对象本身。我的建议是如果只是加一两个属性用方式二最省事如果要加方法或者逻辑复杂用方式一。5.2 渲染器与异常处理的配合异常处理是响应链路里容易被忽略的一环。DRF的exception_handler会把异常转换成Responsedef exception_handler(exc, context): if isinstance(exc, Http404): exc exceptions.NotFound() elif isinstance(exc, PermissionDenied): exc exceptions.PermissionDenied() if isinstance(exc, exceptions.APIException): headers {} if getattr(exc, auth_header, None): headers[WWW-Authenticate] exc.auth_header if getattr(exc, wait, None): headers[Retry-After] %d % exc.wait if isinstance(exc.detail, (list, dict)): data exc.detail else: data {detail: exc.detail} set_rollback() return Response(data, statusexc.status_code, headersheaders) return None注意最后返回None的情况非APIException的异常DRF不处理会交给Django的默认异常处理最终返回500页面。如果你想让所有异常都返回统一格式需要自定义exception_handler并在settings里配置EXCEPTION_HANDLER。自定义异常处理器时要注意返回None表示我不处理交给下一个处理器。如果你想让所有异常都返回JSON就不能返回None要自己构造Response。5.3 性能相关的源码细节最后聊几个和性能相关的源码细节这些在官方文档里不会写但实际项目里很关键第一request.data的懒加载是双刃剑。如果视图里多次访问request.data只有第一次会解析后续都是缓存。但如果你在视图里先访问了request.data又访问了request.POST对于JSON请求request.POST是空的可能造成逻辑错误。第二认证器的顺序影响性能。认证器是顺序遍历的如果第一个认证器每次都要查数据库而大部分请求其实用第二个认证器就能认证那第一个认证器的查询就是浪费。建议把最常用、最轻量的认证器放前面。第三渲染器的render方法会被每个响应调用。如果渲染器里有复杂逻辑会拖慢所有接口。渲染器应该只做格式转换不做业务逻辑。第四finalize_response里的get_renderer_context每次都会创建新字典。如果视图很多、响应很频繁这个字典创建的开销累积起来也不小。不过这个开销相对数据库查询来说可以忽略不用过度优化。6. 读源码之后我改掉的几个坏习惯读完这块源码之后我在实际项目里改掉了几个之前习以为常的写法这里分享出来你可以对照看看自己有没有类似的问题。第一个坏习惯在视图里手动判断请求方法。以前写APIView时喜欢在get方法里再判断request.method其实dispatch已经根据方法名分发了get方法只会被GET请求调用。这个习惯来自写原生Django视图时的思维定式用DRF后应该改掉。第二个坏习惯在认证器里做权限判断。认证器只负责你是谁权限类负责你能不能做。把权限逻辑写在认证器里会导致认证和权限耦合后续想换权限策略时很麻烦。正确的做法是认证器只返回用户身份权限判断交给权限类。第三个坏习惯直接返回字典而不是Response。有些视图里直接return {code: 0}DRF会帮你包装成Response但这样会跳过一些处理逻辑。显式返回Response更清晰也方便后续加headers或status。第四个坏习惯在渲染器里做数据脱敏。数据脱敏应该在序列化器里做渲染器只做格式转换。在渲染器里脱敏会导致 browsable API页面和JSON响应脱敏逻辑不一致而且渲染器拿到的data结构可能因异常处理而不同容易出错。第五个坏习惯忽略request.accepted_renderer。有时候需要在视图里根据客户端期望的格式做不同处理比如JSON返回精简字段HTML返回完整字段。这时候可以读request.accepted_renderer和request.accepted_media_type但要注意这两个属性是在initial阶段设置的在视图方法里可以安全访问。源码读到这里请求响应这条链路基本就通了。下次再遇到request.data拿不到数据request.user是匿名响应格式不对这类问题你应该能直接定位到是哪个环节出了问题而不是靠猜和试。