Files
zijunchiang 698d60bb65 docs(proxy): 记录 Gitea 附件上传上限的两阶段实测(IIS 放开后仍是 500)
按文档 §5.7 硬性要求 4「若用 A 方案,先在发布文档里记录实测上限」,把这次
Windows v0.2.0 整包(38,245,661 字节)发布的实测结果写进 deploy/proxy/README.md
第九节,并修正第九节里「只改 IIS 就够」的隐含假设:

第一阶段(IIS 未放开):14 MB 通过;30.5 / 40 MB 返回 HTTP 413。
第二阶段(IIS 已放开):38,245,661 字节的请求体被**完整接收**(115 s),
不再有 413 —— 说明 maxAllowedContentLength 已经够用;但随后 **Gitea 自己返回
HTTP 500**(响应体是 Gitea 的 API 错误信封,message 为空),瓶颈转移到了 Gitea 侧。

同接口边界测量:914 KB 与 16,372,427 B 通过;16,400,000 B 及以上失败;
且**在若干次大包失败之后,连 100 KB 都会在 0.3 秒内 500** ——
这不符合「上传体积上限」的特征(体积判断会先读完请求体),
更像上传临时目录 / 数据盘写满(Gitea 先写 data/tmp)。

因此文档明确要求运维按顺序查两件事:①Gitea 数据盘剩余空间(含 data/tmp,
以及它是否在小容量 tmpfs 上)并清理失败上传的残留临时文件;
②app.ini 的 [attachment] MAX_SIZE。并在 IIS 配置小节加醒目提示:
若放开 requestLimits 后仍为 500 而非 413,说明请求体已到达 Gitea,
要按上述两件事继续排查。
2026-09-17 01:40:27 +08:00
..

反向代理说明(nginx / IIS)

职责边界:本 Web 端只提供业务服务(NestJS API + 管理端页面), 不部署、不配置反向代理。反向代理由另外的服务或团队负责,本文档只提供配置说明。

安装脚本 deploy/install.sh 不会安装或修改任何反代软件。

一、需要转发的目标(两个端口,按用途分开)

本服务同时监听两个端口,职责不同:

端口 环境变量 默认 给谁用 提供什么
软件 API 端口 PORT 3000 Windows/Godot 客户端、Android App /api、/health、/static、/contracts; 不含管理页面(未知路径 404)
管理端端口 ADMIN_PORT 3001 浏览器(管理员) 管理页面 + 上面全部接口
软件 API:  http://<后端地址>:3000
管理端:    http://<后端地址>:3001

两者都不需要在代理层托管静态文件,也不需要配置 SPA 回退(服务端已处理); 各用一条 location /(nginx)或全匹配规则(IIS)即可。

推荐做法:把两个端口挂在两个不同的站点/域名上,便于分别做访问控制,例如

  • https://dance-api.example.com -> 转发到 3000(给客户端,可用 IP 白名单)
  • https://dance-admin.example.com -> 转发到 3001(给管理员,建议加 Basic Auth / SSO 或只在内网放行)

如果客户端与管理端在同一个内网、也不想配域名,可以让客户端直连 3000(不经过反代), 只把管理端 3001 挂到反代上;反之亦可。

路径 内容 哪个端口提供
/、/assets/* 管理端页面与静态资源 仅管理端;前端路由深链接(如 /songs)由服务端回退到 index.html
/api/* REST API 两个端口都提供(契约见 contracts/rest-api.yaml)
/health 健康检查 两个端口都提供;可用作反代的存活探测,数据库不可用时返回 503
/static/* 上传文件 两个端口都提供(模型 / 音频 / 关键点序列)
/contracts/* 契约快照 两个端口都提供,只读

Windows 客户端与 Android 之间的关键点 WebSocket 链路不经过本服务, 反代无需配置 WebSocket 转发。

ADMIN_PORT 设为与 PORT 相同时是单端口兼容模式:两个端口合一, 管理页面重新出现在 API 端口上(此时反代只需要一条规则)。

二、后端绑定地址怎么选

deploy/.env 中的 HOST(软件 API 端口)与可选的 ADMIN_HOST(管理端端口,缺省跟随 HOST):

部署形态 建议值 说明
反代与后端同机 HOST=127.0.0.1 后端不直接对外暴露,最安全
反代与后端不同机 HOST=0.0.0.0 必须用防火墙把端口限制为仅反代来源可访问
只想让管理端在内网可见 ADMIN_HOST=<内网网卡地址> 与 HOST=0.0.0.0 组合使用,把管理端限制在内网

改完执行 systemctl restart opendance-api(或对应的 ctl.sh restart)生效。

三、必须同步调整的两项

  1. 上传上限:本服务默认允许 512 MiB(UPLOAD_MAX_BYTES)。 代理层默认值通常远小于此(nginx 1 MB、IIS 约 28.6 MB),不改会导致大模型上传失败。
  2. 超时:上传大文件与下载关键点序列可能较慢,代理的读写超时必须放大。

四、nginx 配置

完整样例见 nginx.conf.example。核心内容:

upstream opendance_api {
    server 127.0.0.1:3000;
    keepalive 16;
}

server {
    listen 80;
    server_name your.domain.com;

    client_max_body_size 512m;      # ← 必须放开,否则 VRM/FBX 上传失败
    client_body_timeout 300s;

    location / {
        proxy_pass http://opendance_api;
        proxy_http_version 1.1;
        proxy_set_header Host              $host;
        proxy_set_header X-Real-IP         $remote_addr;
        proxy_set_header X-Forwarded-For   $proxy_add_x_forwarded_for;
        proxy_set_header X-Forwarded-Proto $scheme;
        proxy_set_header Connection        "";

        proxy_connect_timeout 30s;
        proxy_send_timeout    300s;
        proxy_read_timeout    300s;
        proxy_request_buffering off;   # 大文件直接透传,不在代理落盘
    }
}

安装位置与重载:

# Debian / Ubuntu
cp nginx.conf.example /etc/nginx/sites-available/opendance.conf
ln -s /etc/nginx/sites-available/opendance.conf /etc/nginx/sites-enabled/
nginx -t && systemctl reload nginx

# RHEL 系
cp nginx.conf.example /etc/nginx/conf.d/opendance.conf
nginx -t && systemctl reload nginx

五、IIS 配置

IIS 需要先安装两个模块(默认都没有):

  1. URL Rewrite - https://www.iis.net/downloads/microsoft/url-rewrite
  2. Application Request Routing (ARR) - https://www.iis.net/downloads/microsoft/application-request-routing

装完 ARR 后还需启用代理(否则会报 404.4):

IIS 管理器 -> 选中服务器节点 -> Application Request Routing Cache
          -> 右侧 Server Proxy Settings -> 勾选 Enable proxy -> 应用

完整 web.config 样例见 web.config.example,放到站点根目录即可:

<configuration>
  <system.webServer>
    <rewrite>
      <rules>
        <rule name="OpenDanceReverseProxy" stopProcessing="true">
          <match url="(.*)" />
          <action type="Rewrite" url="http://127.0.0.1:3000/{R:1}" />
        </rule>
      </rules>
    </rewrite>
    <security>
      <requestFiltering>
        <!-- 512 MiB:IIS 默认约 28.6 MB,不改大文件上传必失败 -->
        <requestLimits maxAllowedContentLength="536870912" />
      </requestFiltering>
    </security>
    <proxy preserveHostHeader="true" />
    <serverRuntime uploadReadAheadSize="536870912" />
  </system.webServer>
</configuration>

IIS 容易踩的三个坑

  1. requestLimits 与 serverRuntime 要一起改。只改前者,SSL 场景下仍会在 读取请求体时失败(uploadReadAheadSize 默认仅 48 KB)。
  2. 代理超时改的是服务器级配置,不是站点级。ARR 默认 120 秒,上传大文件会 中断。在 applicationHost.config 的 <webProxy> 节点调整:
    <webProxy enabled="true" timeout="00:05:00" />
    
  3. preserveHostHeader 需与 ARR 的 "Preserve host header" 设置一致, 否则后端收到的 Host 会变成 127.0.0.1:3000,影响日志与重定向。

六、HTTPS 与访问控制(可选)

  • HTTPS 终止在反代:本服务只监听 HTTP,证书配置在 nginx/IIS 侧即可。
  • 访问控制:本版本未内置登录认证,如需公网访问,请在反代层加 Basic Auth、IP 白名单或对接已有 SSO。未加任何防护时请只在内网开放。

七、自检清单

配置完成后逐项确认:

# 0. 两个端口都在监听(双端口部署)
ss -tlnp | grep -E ':(3000|3001)'

# 1. 后端自身可用(绕过反代)
curl -i http://127.0.0.1:3000/health
curl -i http://127.0.0.1:3001/health

# 2. 经反代可访问
curl -i http://<反代地址>/health

# 3. 管理端页面可打开(应返回 HTML)
curl -i http://<反代地址>/ | head -5

# 4. 前端深链接可用(应返回同一份 HTML,而不是 404)
curl -i http://<反代地址>/songs | head -5

# 5. API 可用
curl -i http://<反代地址>/api/catalog/version

# 6. 上传上限已放开:找一个小于 UPLOAD_MAX_BYTES 的文件试传
#    若返回 413 说明代理层限制没改
现象 原因
反代 502 / 504 后端未启动,或 HOST 绑定地址不对(同机应为 127.0.0.1,异机应为 0.0.0.0)
页面能开、接口 404 只转发了 /,需要把 /api/、/health、/static/ 一并转发(最简单是全匹配)
上传大文件 413 代理层上传上限没放开(见第三、四、五节)
上传中途断开 代理超时太短
/songs 直接访问 404 SPA 回退被代理拦截;本服务已处理,代理不要自己加 try_files,全匹配转发即可
客户端接口全 404 把客户端的转发规则指到了管理端口之外,或误把 /api 前缀重写掉;客户端应转发到软件 API 端口
管理页面 404 把浏览器的转发规则指到了软件 API 端口(该端口不提供管理页面,这是有意设计);改指 ADMIN_PORT
两个端口都转发到同一条规则 若确实想合并两个端口,设置 ADMIN_PORT=PORT 走单端口模式,而不是让反代去猜
Gitea 的 Release 附件上传 413 这是另一台站点(托管 Gitea 的 IIS),本服务的 UPLOAD_MAX_BYTES 管不到它;见第九节

八、还没有反代时:直连后端怎么访问

在运维侧配好反代之前,可以直接访问后端业务服务(本项目开箱即用,不依赖反代):

  1. deploy/.env 中 HOST=0.0.0.0(若只想让某个网段访问,也可填具体网卡地址)
  2. 防火墙放行后端端口(默认 3000/tcp)
  3. 浏览器访问 http://<服务器IP>:3000

注意:服务默认在 3000 端口,不在 80/443。直接访问 http://<服务器IP>/ 是打不开的--那需要反代把 80/443 转到 3000,属于本节的配置范围。

一条命令定位「服务起来了但外部访问不了」:

bash deploy/check-access.sh                  # 只读诊断,不改配置
bash deploy/check-access.sh /opt/opendance   # 指定安装目录

脚本会打印 .env 中的 HOST/PORT、实际监听地址(重点看是否只监听 127.0.0.1)、回环与对外 IP 两次自测结果、80/443 上有没有反代、 firewalld / ufw / iptables 的放行情况,并直接给出修复命令。


九、Gitea 站点的附件上传上限(Windows 客户端整包必须放开)

本节与第一~五节是两个不同的站点:前者是本 Web 服务的反代,后者是托管 Gitea(源码仓库 + Release 附件)的站点。Windows 客户端的发布整包走 Gitea, 因此必须单独放开 Gitea 站点的上限。

9.1 实测结论(两阶段)

第一阶段(IIS 上限未放开时)

上传体积 结果
14 MB 通过(HTTP 201)
30.5 MB 失败 HTTP 413(IIS「请求实体过大」页面)
40 MB 失败 HTTP 413

第二阶段(IIS 上限放开之后) —— 放开的效果是可验证的: 38,245,661 字节的请求体被完整接收(耗时 115 s),不再出现 413, 说明 IIS 的 maxAllowedContentLength 已经够用。但随后 Gitea 自己返回 HTTP 500 (响应体是 Gitea 的 API 错误信封 {"message":"","url":".../api/swagger"}, message 为空)——即瓶颈已从 IIS 转移到 Gitea 侧。

同阶段的边界测量(同一个 Release 的资产上传接口):

上传体积 结果
914 KB(web v0.7.1 tar.gz) 通过
16,372,427 B(android v0.2.0 zip) 通过
16,400,000 B HTTP 500
16,500,000 B / 18 MB / 21 MB / 24 MB HTTP 500
100,000 B(在第一轮失败之后) HTTP 500,且仅耗时 0.3 s

最后一行是关键:在若干次大包失败之后,连 100 KB 都会在 0.3 秒内 500 —— 这不符合"上传体积上限"的特征(上限判断会先读完请求体), 更像上传临时目录 / 数据盘已满(Gitea 先把请求体写入 data/tmp, 失败时可能在磁盘上留下残留临时文件)。

因此运维侧需要按顺序检查两件事:

  1. Gitea 数据盘的剩余空间(尤其是 data/tmp,以及它是否落在小容量的 tmpfs 上),并清理失败上传留下的临时文件;
  2. Gitea app.ini 的 [attachment] MAX_SIZE(下文第 3 条)。

只放开 IIS 是不够的:本节第 1 步之后仍然 500,说明还有 Gitea 侧的限制或磁盘问题。

→ 默认上限就是 IIS 的 maxAllowedContentLength = 30,000,000 字节(≈28.6 MiB)。 Windows 客户端整包约 38 MB(OpenDance.exe ≈104 MB 未压缩 + OpenDance.pck), 早先因此被拆成 zip.part0/1/2(每卷 13 MiB)才传上去 —— 用户拿到的是 无法直接解压的碎片,必须自己拼接,属于交付缺陷。

9.2 正确做法:放开 Gitea 站点上限到 256 MB

  1. IIS(托管 Gitea 的站点根目录 web.config):

    <configuration>
      <system.webServer>
        <security>
          <requestFiltering>
            <!-- 256 MiB;默认 30,000,000 字节,Windows 整包会被 413 掉 -->
            <requestLimits maxAllowedContentLength="268435456" />
          </requestFiltering>
        </security>
        <serverRuntime uploadReadAheadSize="268435456" />
      </system.webServer>
    </configuration>
    

    uploadReadAheadSize 必须一起改(SSL 场景下默认仅 48 KB,会中途失败)。

  2. ARR(服务器级,不是站点级)在 applicationHost.config 中同步放宽请求体上限:

    <system.webServer>
      <proxy>
        <forwarding />
      </proxy>
    </system.webServer>
    <webProxy enabled="true" timeout="00:05:00" />
    <serverRuntime uploadReadAheadSize="268435456" />
    

    并在 IIS 管理器 → Application Request Routing Cache → Server Proxy Settings 中确认请求体上限(maxRequestBytes)≥ 268435456。

  3. Gitea 自身 app.ini:

    [attachment]
    ; 默认通常为 4~32 MB,必须一起放大,否则 IIS 放行了 Gitea 仍会拒绝
    MAX_SIZE = 256
    
  4. 若 Gitea 前面还有 nginx(IIS 之外或之内):

    client_max_body_size 256m;
    proxy_read_timeout 300s;
    proxy_send_timeout 300s;
    
  5. 改完执行 iisreset /noforce(或只重启站点),再按下节复验。

注意:requestLimits 只是第一道闸。IIS 放开后若仍为 500(而不是 413), 说明请求体已经到达 Gitea,此时要按 9.1 的两件事继续排查 (Gitea 数据盘空间 / [attachment] MAX_SIZE)。

9.3 复验方法

# 造一个 40 MB 文件上传到 Gitea 的附件接口,预期 HTTP 201(不再是 413)
head -c 40000000 /dev/urandom > /tmp/probe.bin
curl -i -X POST \
  -H "Authorization: token <token>" \
  -F "file=@/tmp/probe.bin" \
  "https://<Gitea 地址>/api/v1/repos/<owner>/<repo>/releases/<id>/assets?name=probe.bin"

在放开上限之前,不要用分卷绕过:Windows 端 build/package-release.sh 会拒绝生成 .partN,超限时直接告警并要求先放宽本节的配置。