#!/usr/bin/env python3 # -*- coding: utf-8 -*- """HTTP 语义中间件的**装配**(Bug 28PuqnfpWzB2ypJoUTU_N / R17-2)。 问题 ---- 404/405/400 三类语义修复写在 `apps/yuanjing/app/http_semantics_middleware.py`, 但目标机实际运行入口是 `app/scense.py`(`ps -ef` 只有一个 `venv/bin/python app/scense.py` 进程),`apps/yuanjing` 侧的装配代码 (`app/yuanjing.py` 的 `_register_http_semantics`)从未执行,conf/config.json 的 `middlewares` 也是 null → 语义修复在目标机从未生效: 不存在端点仍 500、DELETE/PUT 仍 200、缺必填参数仍 200/500。 本模块把「装配」抽成**与入口无关**的一个函数,任何入口(scense.py / yuanjing.py) 都能调用: from app.semantic_wiring import install_semantic_middlewares install_semantic_middlewares(app) # aiohttp Application 装配顺序(= append 顺序,外层优先) ----------------------------------- 1. resource_preflight_middleware —— 路由前置 404(Bug P-Wn8N / WXXvX): 在鉴权之前判定 wwwroot 下资源是否存在,不存在直接 404(fpath is None 兜底前置化)。 2. auth_required_middleware —— 未登录访问已存在 .dspy → 401(Bug WhOYy9nvG): 端点若不在公开白名单且无会话凭据,直接 401,不再放行到业务处理器。 3. write_endpoint_semantics_middleware —— 写类端点 GET → 405 / POST 无参 → 400 (Bug CQIpIo / YSNA7 / Zhpd9 的 26+ 条 sqlor 1364 泛化 500)。 4. resource_notfound_middleware —— 异常侧 404 兜底(保留原有覆盖)。 5. http_semantics_middleware —— 405/400/404 既有实现(保留原有覆盖)。 6. business_status_middleware —— 业务信封 {"status":400,...} 回写 HTTP 状态行 (Bug DGzTjOkr / F12 / F16)。 注:真实入口 `app/scense.py` 的构造期装配只按名字加载两项 (`resource_notfound_middleware` / `http_semantics_middleware`),因此 `resource_notfound_middleware` 自身已内建**组合链**(`compose_guard_chain()`), 把上面 1/2/3/6 四层串在自己内部 —— 两条装配路径(构造期 / 钩子期)都能拿到完整语义。 本模块列全 6 项是为了在钩子路径下按名称核验「语义层是否就位」;重复装配无副作用 (幂等:405/401/404 命中即早返回,400 回写只在 status==200 时生效)。 契约 ---- * 幂等:重复调用不会重复 append(按函数对象去重)。 * 容错:任一注册失败只打印 ERROR + traceback 并返回 ok=False,**不抛出**—— 装配点在 ahapp_built 钩子里,抛错会让 run_app 失败、整机启不起来。 * 可核验:装配结果落成进程内记录 + `logs/http_semantics_wiring.json` 证据文件 (middlewares 条数 / 名字 / 是否含全部语义中间件),供部署后自查与复验取证。 """ import json import logging import os import traceback log = logging.getLogger("yuanjing.semantic_wiring") EVIDENCE_BASENAME = "http_semantics_wiring.json" # 本应用负责装配的语义中间件(顺序 = append 顺序 = 外层优先) SEMANTIC_MIDDLEWARE_NAMES = ( "resource_notfound_middleware", "http_semantics_middleware", "business_status_middleware", ) def _mw_name(mw): return getattr(mw, "__name__", getattr(mw, "__qualname__", repr(mw))) def _evidence_path(): app_root = os.path.dirname(os.path.dirname(os.path.abspath(__file__))) return os.path.join(app_root, "logs", EVIDENCE_BASENAME) def write_evidence(payload): """把装配结果写到 logs/http_semantics_wiring.json(失败不影响启动)。""" path = _evidence_path() try: os.makedirs(os.path.dirname(path), exist_ok=True) with open(path, "w", encoding="utf-8") as fp: json.dump(payload, fp, ensure_ascii=False, indent=2) return path except OSError as exc: # noqa: PERF203 log.error("[wiring] 证据文件写入失败 %s: %s", path, exc) return None def _load_middlewares(): """导入全部语义中间件(顺序即装配顺序)。""" from app.route_guard_middleware import ( auth_required_middleware, resource_preflight_middleware, write_endpoint_semantics_middleware, ) from app.notfound_middleware import resource_notfound_middleware from app.http_semantics_middleware import http_semantics_middleware from app.business_status_middleware import business_status_middleware # [fix Zhpd9MXyeGtznDMqini6X / R50] 响应契约守卫必须装配在**链尾(最内层)**: # 只有贴住路由处理器才能看到「处理器返回值本身」。R50 目标机日志实测链尾 # 返回的是 function 对象(aiohttp_session/__init__.py:196 # `RuntimeError: Expect response, not `),异常上抛后被 # 改写成鉴权表象,导致 38 条 500 一条也判不出来。守卫在链尾救援该调用并 # 把契约破坏归因为带处理器身份的 500(见 app/response_contract_guard.py)。 from app.response_contract_guard import response_contract_guard return (resource_preflight_middleware, auth_required_middleware, write_endpoint_semantics_middleware, resource_notfound_middleware, http_semantics_middleware, business_status_middleware, response_contract_guard) def install_semantic_middlewares(app, entry=None): """把 404/401/405/400 语义中间件装进 aiohttp app,返回装配结果 dict。 :param app: aiohttp Application(通常来自 ahapp_built 钩子) :param entry: 入口名(仅用于证据/日志,如 "app/scense.py") :return: {"ok": bool, "middlewares": int, "names": [...], "missing": [...], "entry": ...} """ result = {"ok": False, "middlewares": 0, "names": [], "missing": [], "entry": entry or "", "error": ""} try: # `python app/scense.py` 时 sys.path[0] 是 app/ 子目录, # `from app.notfound_middleware import ...` 需要应用根在 sys.path 上。 import sys app_dir = os.path.dirname(os.path.abspath(__file__)) app_root = os.path.dirname(app_dir) for p in (app_root, app_dir): if p not in sys.path: sys.path.insert(0, p) middlewares = _load_middlewares() have = {_mw_name(m) for m in (getattr(app, "middlewares", None) or [])} added = [] # aiohttp 的 Application.middlewares 只有 property、**没有 setter**: # 旧实现 `app.middlewares = mws` 必然 AttributeError,被 except 吞掉后 # 装配"看似完成"实则未生效(Bug Wp6gXrrEquzg3njONz60b 的静默降级路径)。 # 正确做法逐条 append;app 已 freeze(_make_handler 已构建中间件链)时 # append 会抛 RuntimeError —— 必须显式标记 frozen 由调用方降级,不得当成功。 try: for mw in middlewares: if _mw_name(mw) in have: continue app.middlewares.append(mw) have.add(_mw_name(mw)) added.append(_mw_name(mw)) except RuntimeError as exc: present = [_mw_name(m) for m in (getattr(app, "middlewares", None) or [])] result["frozen"] = True result["names"] = present result["middlewares"] = len(present) result["missing"] = [n for n in SEMANTIC_MIDDLEWARE_NAMES if n not in present] result["error"] = "app 已 freeze,中间件链已构建,append 不生效:%s" % (exc,) log.error("[wiring] app 已 freeze,语义中间件无法生效 entry=%s: %s", entry, exc) written = write_evidence(result) if written: result["evidence"] = written return result names = [_mw_name(m) for m in app.middlewares] missing = [n for n in SEMANTIC_MIDDLEWARE_NAMES if n not in names] result.update(ok=not missing, middlewares=len(names), names=names, missing=missing, added=added) if missing: log.error("[wiring] 语义中间件缺失 %s(已装 %s)入口=%s", missing, names, entry) else: log.info("[init] middlewares=%d semantic=%d added=%s entry=%s", len(names), len(SEMANTIC_MIDDLEWARE_NAMES), added, entry) except Exception as exc: # noqa: BLE001 result["error"] = "%s: %s" % (type(exc).__name__, exc) log.error("[wiring] 装配失败 entry=%s: %s", entry, exc) log.error("[wiring] traceback: %s", traceback.format_exc()) written = write_evidence(result) if written: result["evidence"] = written return result