net::ERR_HTTP2_PROTOCOL_ERROR

之前处理过 2 Enabled Constant Trigger net ERR_HTTP2_PROTOCOL_ERROR 200 (OK) on Chrome,这次在腾讯云服务器上反向代理的网站上也遇到了这个问题。

在 EdgeOne 上开启了以 HTTP/2 协议进行回源

关闭腾讯云双因素身份验证

每次登录都得收个手机短信验证码,关键信任设备时间最大才只有 7 天,还不支持 Passkey,真是拉胯。为了方便,我选择关闭 2FA。

从 DevTools 网络面板中过滤扩展程序请求

Hexo source/_data 目录中无法使用 NexT 函数的错误

之前为了调整备案文本的显示,把 NexT 模板中 footer 里与备案相关的代码复制到自己的 source/_data/footer.njk 文件了,内容大致为

<div class="beian">
  {{- next_url('https://beian.miit.gov.cn', theme.footer.beian.icp + ' ') }}
  {%- if theme.footer.beian.gongan_icon_url %}
    <img src="{{ url_for(theme.footer.beian.gongan_icon_url) }}" alt="">
  {%- endif %}
  {%- if theme.footer.beian.gongan_id and theme.footer.beian.gongan_num %}
    {{- next_url('https://beian.mps.gov.cn/#/query/webSearch?code=' + theme.footer.beian.gongan_id, theme.footer.beian.gongan_num + ' ') }}
  {%- endif %}
</div>

但构建项目时提示 Unable to call `next_url`, which is undefined or falsey。虽然结果最后是成功的,但总是有个黄色的 ERROR 看着很不爽。

INFO  Start processing
ERROR Process failed: _data/footer.njk
Template render error: (G:\GitHub\enihsyou-blog\source\_data\footer.njk) [Line 6, Column 16]
  Error: Unable to call `next_url`, which is undefined or falsey
    at Object._prettifyError (G:\GitHub\enihsyou-blog\node_modules\.pnpm\nunjucks@3.2.4_chokidar@3.6.0\node_modules\nunjucks\src\lib.js:32:11)
    at G:\GitHub\enihsyou-blog\node_modules\.pnpm\nunjucks@3.2.4_chokidar@3.6.0\node_modules\nunjucks\src\environment.js:464:19
    at Template.root [as rootRenderFunc] (eval at _compile (G:\GitHub\enihsyou-blog\node_modules\.pnpm\nunjucks@3.2.4_chokidar@3.6.0\node_modules\nunjucks\src\environment.js:527:18), <anonymous>:24:3)
    at Template.render (G:\GitHub\enihsyou-blog\node_modules\.pnpm\nunjucks@3.2.4_chokidar@3.6.0\node_modules\nunjucks\src\environment.js:454:10)
    at Hexo.njkRenderer (G:\GitHub\enihsyou-blog\node_modules\.pnpm\hexo@7.3.0_chokidar@3.6.0\node_modules\hexo\dist\plugins\renderer\nunjucks.js:60:29)
    at Hexo.tryCatcher (G:\GitHub\enihsyou-blog\node_modules\.pnpm\bluebird@3.7.2\node_modules\bluebird\js\release\util.js:16:23)
    at Hexo.<anonymous> (G:\GitHub\enihsyou-blog\node_modules\.pnpm\bluebird@3.7.2\node_modules\bluebird\js\release\method.js:15:34)
    at G:\GitHub\enihsyou-blog\node_modules\.pnpm\hexo@7.3.0_chokidar@3.6.0\node_modules\hexo\dist\hexo\render.js:74:28
    at tryCatcher (G:\GitHub\enihsyou-blog\node_modules\.pnpm\bluebird@3.7.2\node_modules\bluebird\js\release\util.js:16:23)
    at Promise._settlePromiseFromHandler (G:\GitHub\enihsyou-blog\node_modules\.pnpm\bluebird@3.7.2\node_modules\bluebird\js\release\promise.js:547:31)
    at Promise._settlePromise (G:\GitHub\enihsyou-blog\node_modules\.pnpm\bluebird@3.7.2\node_modules\bluebird\js\release\promise.js:604:18)
    at Promise._settlePromise0 (G:\GitHub\enihsyou-blog\node_modules\.pnpm\bluebird@3.7.2\node_modules\bluebird\js\release\promise.js:649:10)
    at Promise._settlePromises (G:\GitHub\enihsyou-blog\node_modules\.pnpm\bluebird@3.7.2\node_modules\bluebird\js\release\promise.js:729:18)
    at _drainQueueStep (G:\GitHub\enihsyou-blog\node_modules\.pnpm\bluebird@3.7.2\node_modules\bluebird\js\release\async.js:93:12)
    at _drainQueue (G:\GitHub\enihsyou-blog\node_modules\.pnpm\bluebird@3.7.2\node_modules\bluebird\js\release\async.js:86:9)
    at Async._drainQueues (G:\GitHub\enihsyou-blog\node_modules\.pnpm\bluebird@3.7.2\node_modules\bluebird\js\release\async.js:102:5)
INFO  Files loaded in 361 ms

源码调试一波,最终是定位到原因

  1. 在刚启动后,hexo/lib/plugins/processor/data.ts · hexojs/hexo 会加载 source/_data/ 下的文件并按后缀名 render() 一遍
  2. 看上去渲染结果最后被 hexo/lib/hexo/index.ts · hexojs/hexo 放到 locals 中给后续动作用,但这个时候还没加载主题插件,也就拿不到 next_url,产生错误
  3. 后续加载主题,会把函数注册上再重新渲染一遍,最终得到正确的结果

知道原因后解决方式就有了,虽然 NexT文档 说应该那么放,但有时得反着来,不要把模板文件放在 source/_data/ 下 😅

_config.next.yml
custom_file_path:
  head: source/_data/head.njk
  footer: source/_datanext/footer.njk

从外部连接容器中的 Redis

Docker

之前 Umami 服务是用 --extra-host redis:host-gateway 的方式连接部署在主机上的 Redis-Server。运行起来感觉多个服务共用同一个实例总是不好的,就感觉会冲突。虽然有 DB 概念,但大家不用。

host-gatewaydocker container run | Docker Docs 文档中只是简单提到了一嘴,但用来连接主机服务很有用。在 Docker Desktop 中自带了 host.docker.internal

所以现在想的是 docker-compose 内起个 redis。但我又希望从主机能连接到容器中,同时不采用 port-forward 来占用主机的端口。

在 Linux 中 bridge network 默认情况下给每个容器都分配了独立的 IP,主机自带路由可以直连访问容器。同时 redis 的 Dockerfile 智能地禁用了 protected mode 且监听 *:6379

在 ps 中看 Docker 容器的 redis 进程会显示 redis-server *:6379,这个 bind 参数从未在哪指定过却出现了,其实是配置文件中的 set-proc-titleproc-title-template 在起作用。
又因为容器默认不提供任何 redis.conf,所以根据 bind 的文档说明,会监听所有网卡的默认 6379 端口。

这样一来从主机访问 172.18.0.2:6379 就能直达容器,完美解决问题。至于 Docker compose 可以这么写

docker-compose.yml
services:
  umami:
    image: docker.umami.is/umami-software/umami:mysql-latest
    environment:
      REDIS_URL: redis://default:${REDIS_PASSWORD}@redis:6379
    depends_on:
      - redis
    networks:
      - umami_net
  redis:
    image: redis:7-alpine
    command: ["redis-server", "--requirepass", "${REDIS_PASSWORD}"]
    networks:
      - umami_net
 
networks:
  umami_net:

如果想要从宝塔面板管理远程服务器,就需要设置 redis 管理密码。
再在主机上 redis-cli -h $(docker container inspect umami-redis-1 -f '{{.NetworkSettings.Networks.umami_umami_net.IPAddress}}') 连接到容器

Umami IP 定位到南非 Pretoria

在网站上安装了自行部署的 Umami 之后,我的访问用户定位在南非的 Pretoria,很明显是错误的。

Metric definitions – Docs - Umami 文档和 umami/src/lib/detect.ts at master · umami-software/umami 代码上说 IP 地址来自 HTTP Header,但经过那么多 CDN、Proxy 后还能不能留住源 IP 都不好说,Umami 也没个调试功能。
那就部署一个 echo server,httpbin,看看请求过去的 Header 都有哪些。

GET https://httpbin.kokomi.me/headers?show_env=1
 
HTTP/2.0 200 OK
access-control-allow-credentials: true
access-control-allow-origin: *
content-length: 447
content-type: application/json
date: Tue, 14 Oct 2025 09:40:00 GMT
eo-cache-status: MISS
eo-log-uuid: 17097984847967099199
server: nginx
strict-transport-security: max-age=3600;
 
{
    "headers": {
        "Accept": "*/*",
        "Accept-Encoding": "gzip,deflate,br",
        "Cdn-Loop": "TencentEdgeOne; loops=2",
        "Connection": "close",
        "Eo-Connecting-Ip": "2409:8a1e:6941:1d0:361c:744e:1f6e:151d",
        "Eo-Log-Uuid": "677971750601829410",
        "Host": "httpbin.kokomi.me",
        "Remote-Host": "125.94.248.100",
        "User-Agent": "xh/0.25.0",
        "X-Forwarded-For": "2409:8a1e:6941:1d0:361c:744e:1f6e:151d, 125.94.248.100",
        "X-Forwarded-Host": "httpbin.kokomi.me",
        "X-Forwarded-Port": "443",
        "X-Forwarded-Proto": "https",
        "X-Real-Ip": "125.94.248.100",
        "X-Real-Port": "63002"
    }
}

注意请求参数需要添加 show_env=1,才会显示 X-Real-Ip 这类 header。如果不是看到 Include X-Forwarded-For header in headers section · 议题 #300 · postmanlabs/httpbin 完全没有文档说明。还得是在 从源码翻到的 legacy 落地页上 才有说明。

一阵调试最后发现,HTTP Header 传过来的 IP 都是对的,但因为是 IPv6,尝试删除端口号时做了截断 😑 但官方 SaaS 服务又是没问题的,不好说这里有什么代码版本的鸿沟,还是强制 IPv4 了?

src/lib/detect.ts
export async function getLocation(ip: string = '', headers: Headers, hasPayloadIP: boolean) {
  // Ignore local ips
  if (await isLocalhost(ip)) {
    return;
  }
 
  if (!hasPayloadIP && !process.env.SKIP_LOCATION_HEADERS) {
    for (const provider of PROVIDER_HEADERS) {
      const countryHeader = headers.get(provider.countryHeader);
      if (countryHeader) {
        const country = decodeHeader(countryHeader);
        const region = decodeHeader(headers.get(provider.regionHeader));
        const city = decodeHeader(headers.get(provider.cityHeader));
 
        return {
          country,
          region: getRegionCode(country, region),
          city,
        };
      }
    }
  }
 
  // Database lookup
  if (!global[MAXMIND]) {
    const dir = path.join(process.cwd(), 'geo');
 
    global[MAXMIND] = await maxmind.open(
      process.env.GEOLITE_DB_PATH || path.resolve(dir, 'GeoLite2-City.mmdb'),
    );
  }
 
  // When the client IP is extracted from headers, sometimes the value includes a port
 
  const cleanIp = removePortFromIP(ip);
 
  const result = global[MAXMIND].get(cleanIp);
 
  if (result) {
    const country = result.country?.iso_code ?? result?.registered_country?.iso_code;
    const region = result.subdivisions?.[0]?.iso_code;
    const city = result.city?.names?.en;
 
    return {
      country,
      region: getRegionCode(country, region),
      city,
    };
  }
}

知道根因后搜了一下确实有人提到了,而且 master 分支代码已经更新过了。很惊讶这么久居然没支持 IPv6 ?

想要现在就用起来可以自行编译,或者偷懒一下直接上容器改文件 🙂 sed -E 's/e\?\.split\(":"\)\[0\]/e/' .next/server/chunks/5834.js

宝塔面板产生很多 docker-compose logs 进程

一看 ps auxf 超级多的进程在跑,还有重复的。特别是一重启/升级,主进程结束后,全部都上升给 init 当子进程了,更难处理😑

# ps auxf
USER         PID %CPU %MEM    VSZ   RSS TTY      STAT START   TIME COMMAND
root     2844629  0.0  4.6 628420 174280 ?       S    Sep17  11:56 /www/server/panel/pyenv/bin/python3 /www/server/panel/BT-Panel
root     4083852  0.0  1.1 1949080 44368 ?       Sl   Oct13   0:04  \_ docker-compose -f /www/dk_project/dk_app/twikoo/twikoo_N82a/docker-compose.yml logs -f --tail 10
root     4083876  0.0  1.1 1875348 44364 ?       Sl   Oct13   0:04  \_ docker-compose -f /www/dk_project/dk_app/twikoo/twikoo_N82a/docker-compose.yml logs -f --tail 10
root     4086103  0.0  1.1 1875348 44736 ?       Sl   Oct13   0:04  \_ docker-compose -f /www/dk_project/dk_app/twikoo/twikoo_N82a/docker-compose.yml logs -f --tail 10
root     4086118  0.0  1.1 1875348 44340 ?       Sl   Oct13   0:04  \_ docker-compose -f /www/dk_project/dk_app/twikoo/twikoo_N82a/docker-compose.yml logs -f --tail 10
root     4101470  0.0  1.1 1875348 44220 ?       Sl   Oct13   0:04  \_ docker-compose -f /www/dk_project/dk_app/twikoo/twikoo_N82a/docker-compose.yml logs -f --tail 10
root     4101492  0.0  1.1 1875348 44164 ?       Sl   Oct13   0:04  \_ docker-compose -f /www/dk_project/dk_app/twikoo/twikoo_N82a/docker-compose.yml logs -f --tail 10
root     4126193  0.0  1.2 1875348 45920 ?       Sl   Oct14   0:08  \_ docker-compose -f /www/dk_project/dk_app/uptime_kuma/uptime_kuma_MLir/docker-compose.yml logs -f --tail 10
root     4126208  0.0  1.2 1875348 45780 ?       Sl   Oct14   0:08  \_ docker-compose -f /www/dk_project/dk_app/uptime_kuma/uptime_kuma_MLir/docker-compose.yml logs -f --tail 10
root     4126469  0.0  1.2 1875348 45720 ?       Sl   Oct14   0:08  \_ docker-compose -f /www/dk_project/dk_app/uptime_kuma/uptime_kuma_MLir/docker-compose.yml logs -f --tail 10
....

没仔细看代码怎么产生的,但 kill 掉也没发现异常。

DOCKER_COMPOSE_PIDS=$(pgrep -f "docker-compose")
ps -f -p "$DOCKER_COMPOSE_PIDS"
echo "$DOCKER_COMPOSE_PIDS" | xargs kill