Drive API调用SQL存储的pageToken返回400 INVALID_VALUE错误
- 部署了读取Google Drive指定文件夹数据的程序,将每次接口返回的最新
pageToken存储至SQL数据库,循环拉取文件夹内总量超25万的条目 - 程序支持两种运行模式:
- 全量拉取模式:从头启动,迭代调用接口返回的
nextPageToken完成全量数据拉取 - 断点续拉模式:读取SQL表中存储的上一次
nextPageToken值,从断点位置继续执行拉取逻辑
- 全量拉取模式:从头启动,迭代调用接口返回的
- 异常表现:单次运行不依赖SQL存储的token、直接连续迭代接口返回的token时,程序可正常运行直到所有数据读取完成;但读取SQL中存储的token发起接口调用时,会返回400错误请求。该问题在为请求增加
modifiedTime时间范围过滤条件后出现,增加过滤条件前断点续拉流程运行正常。
def get_video_items(folder, service, token=None, begin=None, to=None): # EXTRACT ALL ITEMS FROM SPECIFIED FOLDER if not token: size = PAGESIZE_2 response = service.files().list(q="'" + folder[0]['id'] + "' in parents and modifiedTime > '" + begin + "'and" "modifiedTime < '" + to + "'", orderBy='modifiedTime asc', pageSize=size, fields='nextPageToken, files(id, name, webViewLink)').execute() else: size = PAGESIZE_2 response = service.files().list(q="'" + folder[0]['id'] + "' in parents and modifiedTime > '" + begin + "'and" "modifiedTime < '" + to + "'", orderBy='modifiedTime asc', pageSize=size, fields='nextPageToken, files(id, name, webViewLink)', pageToken=token).execute() videos = response.get('files', []) # RECORD PAGE TOKEN TO CALL NEXT PAGE if len(videos) < size: videos = None else: token = response.get('nextPageToken', None) return videos, token
raise HttpError(resp, content, uri=self.uri)
googleapiclient.errors.HttpError: <HttpError 400 when requesting https://www.googleapis.com/drive/v3/files?q=************+in+parents+and+modifiedTime+%3E+%272019-07-01T16%3A48%3A13.212522%27andmodifiedTime+%3C+%272022-07-01T10%3A38%3A13.212491%27&orderBy=modifiedTime+asc&pageSize=50&fields=nextPageToken%2C+files%28id%2C+name%2C+webViewLink%29&pageToken=%21%21AI9FV7Qu_zWbHy0zBnPqDegUffr0l2XrKvi_U_M8W17yWJ59skMILpiAR-0Oc_i-ZDxrj8Jx3AXBaMrqUh9fnrtxDcBEn4qKTHsdm4Y4_rnc6SJItgaXmOfx7HVjszfmG5z-bOHU-7flG0EIP1a2HIXq-GWFffu2ykGUaDACnteguHEjAe-tNEmPnRR1tHNmNEJACFgWnTnoCXb0w2M9kjA_VXGLwMtQ1Ar-Y9A6IJH3wQXxrIqVrIlWfJ-cEu0z6zGv3saM942Y4O0SKzLyTXk8-SWIozKH5pcFfeBTtn1z_OySgl-lHNIxRgu77lRy9wmgBhCAki8GHL0YjB_QXq7sodq7uwc9VPadokI7__0uoDCjAnvPkQ7gjH7VQWI80a1DadQ0GrZ3&alt=json returned "Invalid Value". Details: "[{'domain': 'global', 'reason': 'invalid', 'message': 'Invalid Value', 'locationType': 'parameter', 'location': 'pageToken'}]"
- 查询语句存在语法错误:原代码拼接
q参数时,begin时间字符串后直接拼接and关键字,缺少空格,最终拼接出的查询条件为modifiedTime > 'xxx'andmodifiedTime < 'yyy',属于非法语法,会触发接口参数校验异常。 - pageToken绑定规则不匹配:Google Drive API返回的
pageToken是和生成时的请求上下文强绑定的临时凭据,仅支持在完全相同的请求参数下使用,以下任意情况都会触发pageToken无效报错:- 续拉时传入的
q过滤规则、pageSize、orderBy、fields任意一项参数,和生成该token时的参数不一致(本次新增modifiedTime过滤条件后,使用旧逻辑生成的历史token发起请求,必然触发报错) - SQL存储字段长度不足:Google Drive返回的pageToken长度普遍超过255字符,如果存储字段设置为
varchar(255),超长部分会被截断,从库中读取的token是不完整的 - 读写过程发生值篡改:存储或读取token时做了额外的URL编解码、特殊字符转义操作,导致传入接口的token值和接口原始返回值不一致
- 续拉时传入的
- 分页判断逻辑存在bug:原代码通过「本次返回条目数是否小于pageSize」判断是否还有下一页,但实际接口可能在返回条目数等于pageSize时已经没有下一页(此时
nextPageToken返回None),原逻辑会错误覆盖token值,甚至把None存入数据库,导致续拉逻辑异常。
- 修复查询语句拼接语法:在
and关键字前后补全空格,避免非法查询语法,推荐使用f-string拼接减少格式错误:q = f"'{folder[0]['id']}' in parents and modifiedTime > '{begin}' and modifiedTime < '{to}'" - 调整SQL存储配置:将存储
pageToken的字段类型设置为TEXT或长度不小于1024的VARCHAR,避免长token被截断;读写token时不做任何额外的转义、编解码处理,保证存入和取出的值和接口原始返回值完全一致。 - 增加参数一致性校验:存储token时,同步把生成该token时用到的所有请求参数(文件夹ID、begin时间、to时间、pageSize、orderBy、fields配置)和token存在同一条记录中;续拉读取token时,先校验当前请求参数和存储的参数是否完全一致,不一致则废弃旧token从头拉取,禁止跨参数场景复用旧token。新增过滤条件后,所有历史生成的旧token直接废弃,不要继续使用。
- 修复分页判断逻辑:放弃用返回条目数判断分页状态,直接以接口返回的
nextPageToken是否存在作为分页终止依据,修正后的代码逻辑如下:
循环调用时只要返回的def get_video_items(folder, service, token=None, begin=None, to=None): size = PAGESIZE_2 q = f"'{folder[0]['id']}' in parents and modifiedTime > '{begin}' and modifiedTime < '{to}'" base_params = { "q": q, "orderBy": "modifiedTime asc", "pageSize": size, "fields": "nextPageToken, files(id, name, webViewLink)" } if token: base_params["pageToken"] = token response = service.files().list(**base_params).execute() videos = response.get('files', []) next_token = response.get('nextPageToken', None) return videos, next_tokennext_token不为None就继续拉取,为None则直接结束分页流程。
内容的提问来源于stack exchange,提问作者Kartikey Mehrotra

