JavaLYG

Python Requests返回404却被当成没有响应:用is None拆开真值与业务判断

接口明明返回了404和错误说明,日志却写“没有响应”?在Python Requests里,if response检查的不是对象是否存在。本文用回环HTTP服务复现这个分支,再把响应对象、HTTP状态、JSON解析和业务成功拆开,避免真正的错误信息被一个布尔判断藏起来。

一、404响应不是None,只是布尔值为False

常见写法是先请求,再用if response决定要不要读取正文。这看起来像在检查“拿到东西没有”,但Requests.Response定义了自己的真值规则。对本文的普通HTTP响应,状态码低于400时真值为True,4xx和5xx时为False;404错误页依然是一个完整的Response对象。

因此,if not response不能解释成“服务器没回包”,也不能直接据此判断网络中断。它可能只是服务器认真回答了“资源不存在”。这时状态码、响应头和错误正文都还在,只是程序自己走错了门。检查可选对象有没有赋值,应明确写response is None或response is not None。

Requests响应对象存在、HTTP状态与业务成功的两列对照图
两列为独立反例,不是执行顺序。左列404对象仍存在;右列200且JSON中的ok为false,仅表示演示业务未通过。普通Response的真值判断不检查JSON内容,也不代替接口契约。

二、别用or把真正的错误响应换掉

这个真值规则也会影响response or fallback。左侧是404响应时,表达式会选择备用对象,原先的错误原因就可能被默认结果覆盖。问题不在or坏了,而在对象的真假含义与调用者想检查的条件不同。需要“只有None才使用备用值”,就写显式条件表达式,不要让状态码替你决定对象是否存在。

异常处理里还有同样的坑:捕获HTTPError之后,如果写if error.response再打印正文,404对应的response仍是假值,于是错误日志又被跳过。应使用error.response is not None。也别因为HTTPError通常带响应,就推广成所有连接异常、超时异常都必然有响应;能拿到多少上下文,要看具体异常实例。

三、raise_for_status不是业务验收按钮

raise_for_status会针对4xx、5xx抛HTTPError,正常返回值是None,不是成功的Response。不要把它的返回值重新赋给response,否则检查通过后变量反而变成None。它也不会仅因302跳转或200里的业务拒绝字段就报错。若接口约定必须返回200,应在HTTP异常检查之外明确比较状态码。

Response.ok也不是“恰好200”的缩写。本例关闭自动跳转时,302的bool和ok都为真,但它没有提供预期业务结果。Requests的GET通常会跟随跳转,调试时应分开看最终状态、最终URL和history。下面特意关闭跳转,目的只是把原始302留在眼前,不是在推荐所有接口都禁用跳转。

四、JSON能解析,与调用成功是两回事

错误响应也可以返回结构正确的JSON。比如404正文里带error字段,json方法完全可以正常解析;反过来,HTTP200可能返回HTML登录页或损坏的数据,解析依然会失败。204空响应也不能凭Content-Type写着JSON就直接解析。媒体类型是线索,实际字节、接口约定和解析结果需要分别核对。

本文固定一个很窄的演示契约:只接受200、JSON对象以及布尔ok严格为true。实际接口可能接受201、204,或用code字段表达结果,必须按自己的文档调整。示例用is True特意拒绝数字1和字符串“true”之类的替代值;这是一条教学业务策略,不是Requests强制所有API采用的格式。

五、复制完整实验,在本机观察14项检查

运行环境需已有Python3和Requests。将下面完整代码保存为response_check.py,再用对应环境的python3 response_check.py执行。它只绑定127.0.0.1随机端口,关闭环境代理继承,并在finally停止线程、关闭监听;不会请求外部业务接口,也不会改系统代理。使用受限环境时,先确认策略允许创建本地监听。

import json
import threading
from http.server import BaseHTTPRequestHandler, HTTPServer
import requests

# 仅演示本机有限响应,不是生产接口的业务验收。
样本 = {
    '/成功': (200, 'application/json', b'{"ok":true}'),
    '/拒绝': (200, 'application/json', b'{"ok":false}'),
    '/缺失': (404, 'application/json', b'{"error":"missing"}'),
    '/故障': (500, 'application/json', b'{"error":"failed"}'),
    '/空体': (204, 'application/json', b''),
    '/跳转': (302, 'text/plain', b'moved'),
    '/网页': (200, 'text/html', b'<p>not json</p>'),
}

class 处理器(BaseHTTPRequestHandler):
    def do_GET(self):
        from urllib.parse import unquote
        状态, 类型, 内容 = 样本[unquote(self.path)]
        self.send_response(状态)
        self.send_header('Content-Type', 类型)
        self.send_header('Content-Length', str(len(内容)))
        if 状态 == 302:
            self.send_header('Location', '/success')
        self.end_headers()
        self.wfile.write(内容)

    def log_message(self, *参数):
        pass

def 判定(响应):
    # 固定教学契约:仅200 + JSON对象 + 布尔ok=true算通过。
    if 响应 is None:
        return '无响应对象'
    if 响应.status_code != 200:
        return '非预期HTTP状态'
    try:
        数据 = 响应.json()
    except ValueError:
        return 'JSON解析失败'
    if not isinstance(数据, dict) or 数据.get('ok') is not True:
        return '业务字段未通过'
    return '通过'

计数 = 0
def 检查(名称, 条件):
    global 计数
    if not 条件:
        raise RuntimeError('检查失败:' + 名称)
    计数 += 1
    print('通过:' + 名称)

服务 = HTTPServer(('127.0.0.1', 0), 处理器)
线程 = threading.Thread(target=服务.serve_forever, daemon=True)
线程.start()
try:
    with requests.Session() as 会话:
        会话.trust_env = False
        地址 = f'http://127.0.0.1:{服务.server_port}'
        响应集 = {}
        预期 = {'/成功':'通过', '/拒绝':'业务字段未通过',
                '/缺失':'非预期HTTP状态', '/故障':'非预期HTTP状态',
                '/空体':'非预期HTTP状态', '/跳转':'非预期HTTP状态',
                '/网页':'JSON解析失败'}
        for 路径, 结论 in 预期.items():
            响应 = 会话.get(地址 + 路径, timeout=(2, 2), allow_redirects=False)
            响应集[路径] = 响应
            检查(路径, 响应 is not None and
                 bool(响应) == (响应.status_code < 400) and 判定(响应) == 结论)
        缺失 = 响应集['/缺失']
        检查('404仍能读取JSON', 缺失.json() == {'error':'missing'})
        检查('or替换了真实404对象', (缺失 or 响应集['/成功']) is 响应集['/成功'])
        try:
            缺失.raise_for_status()
        except requests.HTTPError as 错误:
            检查('异常保留同一响应', 错误.response is 缺失 and
                 错误.response is not None and not bool(错误.response))
        else:
            raise RuntimeError('404未抛出HTTPError')
        检查('200检查正常返回None', 响应集['/拒绝'].raise_for_status() is None)
        检查('302检查正常返回None', 响应集['/跳转'].raise_for_status() is None)
        try:
            响应集['/空体'].json()
        except ValueError:
            检查('204空体不能按JSON解析', True)
        else:
            raise RuntimeError('空体竟能解析')
        检查('None单独处理', 判定(None) == '无响应对象')
finally:
    服务.shutdown()
    线程.join(timeout=3)
    服务.server_close()
print(json.dumps({'检查数':计数, 'Requests版本':requests.__version__,
                  '监听已关闭':服务.socket.fileno() == -1}, ensure_ascii=False))

实验并不是手工填几个Response对象冒充服务器,而是由本机HTTP服务真实返回七种状态和正文,再由Requests访问。七组响应分别核对对象存在、真值和契约分流;另外检查404错误JSON、or替换、异常保留响应、两种raise_for_status返回、204解析失败与None分支。总计14项,重复运行不增加独立检查数量。

六、按故障层次记录,不把错误正文全倒进日志

现象先核对不要推断
if response没进入是否None与status_code分别看没收到HTTP响应
raise_for_status没报错是否预期状态及业务字段业务已成功
json正常返回HTTP状态与对象结构HTTP没有错误
200却解析失败实际内容与登录跳转一定是网络故障
204且正文空接口是否约定无内容成功必须补一个JSON对象

生产排查通常先记异常类型、状态码、脱敏后的目标和服务端请求标识。不要直接记录完整URL查询参数、认证头、Cookie或整段错误正文,里面可能含令牌和个人信息。本文只用自造的短文本,允许逐字核对;这个条件不能不加说明地搬到真实服务日志里。保留上下文,不等于把所有数据都公开。

也不要把失败统一改成空字典或None继续处理。这样上层既分不清网络错误与业务拒绝,也可能把默认空结果当成成功。是否重试应由错误类型、调用幂等性和业务约定决定,不能因为遇到False就再发一次写请求。本实验没有写业务接口,也没有证明重试能撤销任何外部副作用。

七、上线前用自己的接口契约再验一遍

验收时至少准备:正常结果、业务拒绝、4xx错误JSON、5xx错误JSON、跳转、空体和非JSON正文。分别确认日志没有把响应误判成不存在,错误对象没有被or替换,状态策略明确,解析失败有独立分支,业务字段没有被真假混淆。检查通过后,还要验证调用方收到正确结果,而不只是看到终端打印“通过”。

本轮在Linux、CPython3.14.3与Requests2.34.2执行,14项检查通过,结束时监听已关闭。这里的结论仅来自回环HTTP与普通Requests响应,未验证公网TLS、代理、真实认证、流式大响应或生产业务。timeout参数只提供连接和读取等待约束,不承诺整条业务链的总截止时间;这些边界应另设测试,不能借本文顺手宣布全部验收。

如果现有函数把所有异常都捕获后返回None,先修改它的返回约定或异常传播方式,再修上层条件。否则即使用了is None,也只能看到一团已经被包装层抹平的失败,无法补回丢失的状态和正文。可以保留HTTPError向上传递,或者定义结构化错误结果,但不要同时让None代表未调用、网络失败、空结果和业务拒绝四种情况。

为避免测试自己骗自己,正常响应与错误响应必须来自不同样本,且核对实际状态码。只打印一个True或“请求完成”,无法证明错误分支执行过。示例中的检查使用显式异常,不依赖assert,因此优化模式不会悄悄删掉检查。服务关闭只表示本地测试资源释放,并不说明远端操作撤销;读取响应对象也不等于重新发起了一次请求。

八、参考资料与复用要点

最值得保留的是判断问题时先说清“我在检查什么”:对象是否存在用身份判断,HTTP是否符合预期看状态,正文能否读取看解析,操作有没有完成看业务契约。四个问题不要挤在一个if里,代码未必短很多,但下一次出错时,日志终于能说明白是谁没过关。

本文由AI辅助整理;技术事实、完整代码与配图经复核,作者对内容负责。

🔕 评论已关闭