我们知道 Python Requests库 中的 Session 模块有连接池和会话管理的功能,比如请求一个登录接口后,会自动处理 response 中的 set-cookie,下次再请求时会自动把 cookie 带上。但最近出现了一个诡异的事情,cookie 没有自动带上,导致请求 403。
一开始怀疑是登录接口错误了,没有 set-cookie,但抓包发现 response header 中有 set-cookie,打印请求的 response.cookies 也有需要的 cookie。又怀疑是 set-cookie 的格式不对或者其它问题,但用浏览器实际跑了下流程,发现系统一切正常,那基本就是 requests 库的问题了。
没办法,只能 debug 了,单步调试了几轮,基本了解了 requests 的处理方式,首先把请求参数转变为 Request
对象,然后对使用 prepare_request 对 Request
进行预处理,其中有一步 merge_cookies 的操作(还有各种其它处理),把传入的 cookies 和 self.cookies merge 到 RequestsCookieJar
对象上去,这一步也没啥问题,merged_cookies 变量也是对的。后续将预处理过的请求,通过内置的 http adapter 发送出去。http adapter 底层是通过 urllib3.poolmanager
获取到 urllib3.connectionpool
连接(这里是连接池的核心部分),再通过 conn.urlopen
实际发送请求。虽然跟踪了解到了整个请求逻辑,但最终发出的请求还是没有带上需要的 cookie。
问题定位一度陷入僵局,只能再回顾上面的流程,cookie 肯定就是在 merged_cookies 和 conn.urlopen 之间没的,再仔细观察发现,conn.urlopen 请求参数里面压根没有 cookie 字段。
1 2 3 4 5 6 7 8 9 10 11 12 13 | # https://github.com/psf/requests/blob/master/requests/adapters.py#L439 resp = conn.urlopen( method=request.method, url=url, body=request.body, headers=request.headers, redirect=False, assert_same_host=False, preload_content=False, decode_content=False, retries=self.max_retries, timeout=timeout ) |
查阅资料发现,urllib3 的作者说,连接池只处理底层连接,cookie 跟踪等事情应该上层来做。大胆猜测,那 cookie 应该是放在 header 里了,往前捣看看 request.headers 是怎么变动的(此时里面的 Cookie 字段确实不正确)。
再走查代码发现 prepare_request 里面是调用了 PreparedRequest.prepare
,其中有一步 prepare_cookies,主要是调用了 cookielib.CookieJar.add_cookie_header
最终将 cookie 放到了 self.headers['Cookie']。但里面有个 request.has_header("Cookie") 的判断,header 中没有 Cookie 字段才会放,不知道为什么这么考虑(最新版 3.8 还是这样),估计是 merge cookie 比较麻烦,但问题确实就出在这里,这次请求之前,Requests Session 已经直接通过 headers['Cookie'] 设置了 cookie。复现代码(修改自官方示例):
1 2 3 4 5 6 7 8 9 10 11 | import requests s = requests.Session() s.headers.update({ 'Cookie': 'k=v' }) s.get('https://httpbin.org/cookies/set/sessioncookie/123456789') r = s.get('https://httpbin.org/cookies') print(r.text) # 预期:{"cookies":{"k":"v"},"cookies":{"sessioncookie":"123456789"}} # 实际:{"cookies":{"k":"v"}},未包含sessioncookie |
虽然找到了问题的原因,但又不好解决,总不能不让直接操作 headers['Cookie'] 吧,先不说无法限制使用者,而且之前的代码已经这样做了,改动量非常之大。不过好在,现在自己用的框架是在 Requests Session 上封装了一层,操作 header 都是调用的统一的 update_headers 方法:
1 2 3 4 5 6 | def update_headers(self, headers): """ 更新当前会话的header :param headers: header字典 """ self.headers.update(headers) |
对 headers['Cookie'] 的操作拦截一下,变成对 cookies 的操作:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 | def update_headers(self, headers): """ 更新当前会话的header :param headers: header字典 """ for header_key, header_value in headers.items: if header_key == 'Cookie' or header_key == 'cookie': c = Cookie.SimpleCookie() c.load(header_value) cookies = {} for key, morsel in c.items(): cookies[key] = morsel.value requests.utils.add_dict_to_cookiejar(self.cookies, cookies) del headers[header_key] self.headers.update(headers) |
这样即不用改之前的调用方代码,也防止了后人掉坑。
参考资料
https://requests.readthedocs.io/en/master/user/advanced/#session-objects
https://stackoverflow.com/questions/2422922/python-urllib3-and-how-to-handle-cookie-support
https://urllib3.readthedocs.io/en/latest/reference/index.html#module-urllib3.poolmanager