故障排除

你最可能遇到的错误 — 端口、权限、TLS、缺少 NIP、发布和超时 — 附分步修复方法。

首先检查三件事:

  • nostrfy check 校验你的配置(大多数错误是配置失误)。
  • tail -f nostrfy.log 显示日志 — 原因几乎总在那里。
  • nostrfy restart 干净地重启守护进程。

无法启动

error: cannot bind to 0.0.0.0:80: Permission denied

原因:80 端口只能由 root 绑定。

修复:用 sudo 运行,或将端口改为 8080 之类。

sh
# 在配置文件中将 port = 8080 改掉,然后:
nostrfy --config nostrfy.toml start

error: cannot bind to ...: Address already in use

原因:另一个进程(旧的 nostrfy 或不同的服务器)已经占用了 该端口。

修复:

sh
ss -tlnp | grep :8080
sh
# 如果 nostrfy 正在运行,重启它
nostrfy --config nostrfy.toml restart

already running (pid 1234); use 'nostrfy stop' or 'nostrfy restart'

原因:nostrfy 已在运行;start 拒绝启动第二个 实例。

修复:使用 nostrfy restart,或直接使用正在运行的实例。

nostrfy stop 卡住 / did not stop in time

原因:守护进程卡住或无响应。

修复:

sh
ps aux | grep nostrfy
kill -9 <PID>
# 如果存在陈旧的 pid 文件,删除它
rm -f nostrfy.pid

error: invalid nostrfy.toml: TOML parse error

原因:配置文件不是有效的 TOML。常见错误:忘记给字符串加引号, 或同一个键写了两次。

修复:错误消息包含行号。检查并修复该行。

toml
# 正确示例
name = "my relay"        # 字符串用 " 引起来
port = 8080              # 数字直接写
enabled_nips = [1, 50]   # 列表用 [ ] 包裹

error: cannot read nostrfy.toml: No such file or directory

原因:配置文件不存在。

修复:

sh
nostrfy --config nostrfy.toml init

error: relay.private_key is not a valid secp256k1 secret key

原因:relay.private_key 不是有效的 64 字符十六进制密钥。

修复:运行 nostrfy genkey 生成正确的密钥(或设置 private_key = "")。

启动时日志中有大量警告

[WARN] 日志行告诉你配置问题。主要的:

警告含义和修复
relay.public_url is empty and server.host is "0.0.0.0"...未设置 public_url — NIP-42 认证、NIP-62 消除和 NIP-98 管理认证 将无法工作。设置 wss://your-public-url。
relay.private_key is empty while NIP-29 is enabled...群组需要密钥。运行 nostrfy genkey。
unknown config key [relay].software is ignored配置中有未使用的遗留键(或拼写错误)。检查键名。
unknown config section [serve] is ignored段名拼写错误(例如 [serve] 而不是 [server])。 修复它。
relay.require_auth is true but relay.send_auth_challenge is false...这个组合会把所有人锁在外面。更改其中之一。
relay.require_pow = 64 ... practically unmineablePoW 要求太高,没人能发布。降低 require_pow。

无法连接或行为异常

客户端收到 connection refused

原因:中继未运行,或防火墙阻止了端口。

修复:

sh
curl http://127.0.0.1:8080/health

# 从外部(使用服务器的 IP/端口)
curl http://YOUR_SERVER_IP:8080/health

# 检查防火墙(示例:ufw)
sudo ufw status
# 如需要,开放端口
sudo ufw allow 8080

外部客户端无法连接,本地可以

原因:server.host 仍是 127.0.0.1(默认), 它只接受本地连接。

修复:在配置中设置 host = "0.0.0.0" 并重启。

无法通过 Cloudflare Tunnel 连接

使用 Cloudflare Tunnel 时:

  • 中继运行普通 HTTP;Cloudflare 终止 TLS,因此客户端使用 wss://。在中继上设置 public_url = "wss://..."(这使 NIP-42 认证正常工作)。
  • Cloudflare 会添加 X-Forwarded-Proto 头。nostrfy 对 ws/wss/http/https 值一视同仁,因此通常 无需额外配置。

error: message too large 且连接关闭

原因:单条消息超过 max_ws_message_bytes(默认 1 MB)。

修复:如果需要更大的事件,提高 limits.max_ws_message_bytes — 但 也要检查客户端自己的限制。

too many subscriptions / too many filters 错误

原因:达到了每连接上限(订阅默认 20,过滤器 默认 20)。

修复:提高 limits.max_subscriptions / limits.max_filters(并检查客户端设置)。

负载下新连接被拒绝

原因:达到了 max_connections(默认 10000)、触发了按 IP 上限 (max_connections_per_ip,默认 64),或每秒连接速率限制 (max_connections_per_sec_per_ip)拒绝了突发。这些上限适用于每个连接 — WebSocket 和普通 HTTP 一样。

修复:检查并调整设置。max_connections_per_ip = 0 禁用 按 IP 上限;max_connections_per_sec_per_ip = 0 禁用速率限制。这三项 设置需要重启。

连接一段时间后断开

原因:如果设置了 ws_idle_timeout_secs,空闲连接会被关闭。 健康的客户端会用 PONG 回应中继的 PING 并保持连接;只有死掉的对端会被清理。

修复:这是有意为之 — 默认 300 秒。设置 ws_idle_timeout_secs = 0 完全禁用它。

订阅以 CLOSED ... response too large 结束

原因:一个 REQ 的已存储事件超过了 max_req_response_bytes (默认 32 MiB)。只在事件非常大或过滤器非常宽时发生。

修复:缩小过滤器(更严格的 since/until、更低的 limit)或提高 max_req_response_bytes(0 禁用预算)。

NIP-11 supported_nips 列表中缺少某个 NIP

原因:公布的列表是动态的 — 当某个 NIP 定义的所有类型都被 拒绝时它会被隐藏:它们都在 blocked_kinds 中、都不在 allowed_kinds 中,或它们是 reject_ephemeral 拒绝的临时类型。 NIP-29/43/66 还需要 relay.private_key,NIP-86 需要 rpc.management_token 或 rpc.admin_pubkey。

修复:检查活动的访问列表 — NIP-86 listallowedkinds 显示 类型白名单,GET / 立即显示生效的 supported_nips。 移除屏蔽的类型或 reject_ephemeral 设置。

发布时的错误

发布失败时,OK 消息的第 4 个元素解释原因。常见的:

错误含义和修复
invalid: signature verification failed事件签名无效(可能是客户端密钥损坏)。
invalid: content too large内容超过 max_content_bytes(默认 64K 字符)。缩短它或 提高限制。
invalid: too many tags标签数超过 max_tags(默认 2000)。
invalid: event creation date is in the future时间戳过于超前(超过 max_created_at_future_secs)。
mute: event contains secret key material内容或标签包含类似 nsec 的字符串。 切勿发布密钥。移除该字符串后事件会被接受。
duplicate: event already stored同一事件已存储(正常)。
blocked: pubkey not allowed该公钥被封禁(banpubkey)或在白名单之外。
blocked: kind not allowed此类型被禁止。
rate-limited: too many events该公钥超过了 max_events_per_min_per_pubkey(滑动 60 秒 窗口)。等一分钟再试,或提高/禁用该限制。
blocked: event has been banned该事件 id 被封禁。
blocked: event has been deleted重新发布已删除的事件。
auth-required: ...需要认证(当 relay.require_auth 开启时)。
restricted: your account is too new账户创建时间在 new_pubkey_min_age_secs 之内。等待并重试。
restricted: unknown group群组不存在(先创建它)。
restricted: this group is closed群组是 closed;没有邀请码的加入请求不会被处理。

Blossom 文件服务器

上传失败并返回 401

上传授权事件(kind 24242)被拒绝。检查:

  • 令牌的 expiration 标签存在且设置为未来的 unix 时间戳,
  • 对于 upload/media/delete,令牌带有包含 blob 的 sha256 的 x 标签,
  • server 标签(如果存在)正好是配置的 blossom.host (仅主机名,无 scheme/path),
  • 令牌是在最近 10 分钟内签名的(防止重放的新鲜度窗口),
  • 且签名密钥是上传者自己的。

上传失败并返回 403

设置了 blossom.restrict_uploads = true 且该公钥不在白名单中 — 用 nostrfy blossom allow npub1... 添加它(守护进程会自动重载)。如果列表看起来 不对,nostrfy blossom list 会显示它。

上传失败并返回 409

客户端发送的 X-SHA-256 头与实际请求体不匹配( 声明的哈希是在不同字节上计算的 — 例如文件在哈希和发送之间发生了变化)。 客户端可以完全省略该头。

媒体主机上的 GET / 返回 NIP-11 文档

请求没有带着 Blossom Host 头到达中继。在反向代理中将 media.example.com (或 blossom.host 设置的值)指向同一端口,然后 nostrfy restart。

blob 在上传后立即 404

文件以其 SHA-256 内容寻址:使用上传响应中返回的确切哈希获取它 (/<sha256> 或 /<sha256>.<ext>)。不匹配意味着 客户端请求的哈希与它发送的字节不同。

搜索、群组和认证

搜索返回 0 条结果 / 意外结果

nostrfy 搜索匹配全词。注意:

  • search = "rust" 匹配包含单词 "rust" 的事件,但 "ru" 不会作为子串匹配 "rust"。
  • 只搜索事件内容中的单词。
  • 如果 search_index = false,搜索仍然可用但更慢。
  • 如果 NIP-50 被禁用(disabled_nips = [50]),search 被忽略 (会发送 NOTICE)。

群组元数据(39000-39005)没有生成

原因:未设置 relay.private_key。群组快照由 中继自己的密钥签名,因此没有它就不会生成任何东西。

修复:

sh
nostrfy --config nostrfy.toml genkey
nostrfy --config nostrfy.toml restart

restricted: unknown group 拒绝群组事件

原因:群组不存在。在 NIP-29 中,管理事件和加入请求 (9021)不能在群组创建(kind 9007)之前针对它。

修复:先用一个 9007 事件创建群组。

restricted: you are not an admin of this group

原因:管理操作(添加成员等)需要管理员(带角色的成员)。 创建者是管理员。

修复:请管理员授予你角色,或创建你自己的群组。

restricted: this group is closed

原因:群组是 closed;没有邀请码的加入请求 不会被自动批准。

修复:向管理员索取邀请码(9009)并用 code 标签加入。

不小心离开了群组,或群组没有管理员

原因:NIP-29 离开请求(kind 9022)对任何成员都被处理 — 包括 群组的最后一位管理员,他离开后不会留下管理员。没有管理员,就没人能发送管理事件 (9000/9001/9002/9008)了。

修复:用中继自己的密钥(relay.private_key, 即 NIP-11 self 公布的公钥)签署一条管理事件。根据 NIP-29,管理事件可以来自 "中继主密钥或……群组管理员",因此即使群组没有管理员,中继也接受由自己的密钥签名的群组管理事件。 例如,用 kind:9000 恢复一位管理员:

json
{
  "kind": 9000,
  "pubkey": "<relay self pubkey>",
  "tags": [["h", "<group-id>"], ["p", "<member-hex>", "admin"]]
}

用中继密钥签名并发布它。或者,用中继签名的 kind:9008 删除群组(其存储的事件被清除),然后用 kind:9007 重新创建。 此恢复需要配置 relay.private_key。

受保护事件被 auth-required 拒绝

原因:NIP-70 受保护事件(带 - 标签)只能由 已认证的作者在同一连接上发布。

修复:发布前在客户端启用 NIP-42 认证。

AUTH(NIP-42)返回 false

常见原因:

  1. 未设置或错误设置 relay.public_url — AUTH 事件的 relay 标签与 中继的 URL 不匹配。设置 wss://... 并重启。
  2. 挑战过期 — 你在不同的连接上发送了 AUTH,或复用了旧的挑战。
  3. 客户端时钟偏差 — AUTH 事件的 created_at 必须在 现在的 ±10 分钟内。

NIP-86 管理 API 返回 401 unauthorized

原因:凭据缺失或错误。

修复:

  • 设置 management_token 并发送 Authorization: Bearer <token>。
  • 或设置 admin_pubkey 并发送 NIP-98 认证事件(u 标签必须与 中继 URL 完全匹配;需要 payload 标签)。
  • 如果两者都未设置,管理 API 完全禁用。

NIP-98 认证事件因 scheme 或端口不同被拒绝

NIP-98 规范说 u 标签必须与绝对请求 URL 完全相同,因此 nostrfy 从 relay.public_url 推导期望的 URL:其 authority 加上 从 WebSocket scheme 映射的 HTTP scheme(wss:// → https://, ws:// → http://,去掉 nostr+)。没有 public_url 时,中继期望它提供的普通 http://host:port。带有 其他 scheme、不同/省略端口,或不同路径或查询的标签会被拒绝 — 将 relay.public_url 设置为客户端签名的公开地址。每个认证事件也是 一次性的:在其 60 秒 有效窗口内重放同一个 Authorization 头会被拒绝。

数据库和磁盘

database map is full: increase database.max_map_size

原因:达到了 LMDB 内存映射上限(默认 1 TB 虚拟地址空间;实际 磁盘使用随数据增长)— 实际上就是数据库满了。

修复:提高 database.max_map_size 并重启。

disk is full: refusing to commit N events

原因:可用磁盘空间少于 32 MB。写入停止(以保护数据);读取 继续。

修复:释放磁盘空间。一旦有空间,写入会自动恢复。 (df -h /path/to/data)

nostrfy check 报告 map_size must not exceed max_map_size

原因:database.map_size 大于 max_map_size。

修复:将 map_size 设置为等于或低于 max_map_size(默认值 就没问题)。

检查数据库大小

sh
curl http://127.0.0.1:8080/relay/stats
# => "db_size_bytes" 以字节为单位

备份 / 移动数据库

所有数据都在 database.path 目录中。 复制前先停止中继(复制活动数据库可能损坏它)。

sh
nostrfy --config nostrfy.toml stop
cp -a ./data ./data-backup
# 使用本地 Blossom 存储时,也要备份 [blossom].local_path。
nostrfy --config nostrfy.toml start

守护进程操作

nostrfy stats 显示 nostrfy is not running (no stats file)

原因:统计文件不存在 — 守护进程未运行,或它在几 秒前才启动。

修复:运行 nostrfy start,等几秒,然后重试。

日志无限增长

原因:max_log_size_bytes 为 0(轮转已禁用)。

修复:设置 max_log_size_bytes = 52428800(50 MB)和 max_log_files = 5。轮转自动进行。

重载后配置更改没有生效

原因:你重载(SIGHUP)了启动时固定的设置: private_key、api_host、metrics_enabled、LiveKit 设置,以及 NIP 启用/禁用列表。

修复:使用 nostrfy restart。这种情况下日志包含 "a restart is required" 警告。

中继不断自行死掉

原因:机器重启了,或中继内存不足(OOM)。

修复:

  1. 检查日志末尾:tail -50 nostrfy.log。
  2. 检查机器是否重启:uptime(非常短的运行时间意味着重启)。
  3. 检查内存:free -h。
  4. 再次启动中继:nostrfy start。
提示
要在开机时自动启动 nostrfy,将它注册为 systemd 服务,以中继的启动 命令作为 ExecStart。

systemd 无法在 80 端口启动中继

以 root 运行的 systemd 服务可以绑定 80 端口。如果你把 User= 设为普通用户, 要么使用更高的端口(例如 8080),要么向单元添加 AmbientCapabilities=CAP_NET_BIND_SERVICE。

仍未解决?

  1. 检查日志:tail -100 nostrfy.log — 它通常会指出直接 原因。
  2. 重新校验配置:nostrfy check — 显示警告和错误。
  3. 收集复现细节:你在做什么、哪个客户端、确切的错误。
  4. 在项目仓库提问: https://github.com/iqbqioza/nostrfy — 提交 issue 时,附上复现步骤和日志。