Repository navigation
feat: default user agent - #389
Conversation
改动说明更新:(点击查看)变更概述本次变更为 CustomSkinLoader 的 HTTP 请求添加统一的默认 User-Agent,并允许 loadlist 和 ExtraList 中的自定义 默认 User-Agent 至少包含以下信息:
默认格式如下: 示例: 同时,自定义 User-Agent 可以使用以下占位符: 变更背景CustomSkinLoader 会向 Mojang API、第三方皮肤 API 和皮肤纹理地址发起 HTTP 请求。此前这些请求没有统一的默认 User-Agent,第三方皮肤服务端无法稳定获得客户端的 Minecraft、Mod 加载器、Java 和 CustomSkinLoader 版本信息。 loadlist 虽然已经提供 本次变更的目标是:
最终实现将探测职责下沉到各加载器的 Bootstrap:Bootstrap 在加载 Common 之前通过本加载器 API 取得名称和版本,再把统一结果提供给 Common。这样既修复 Fabric 0.19.2 的版本读取,也使 User-Agent 代码保持与加载器无关。 改动前的行为
c.setRequestProperty("User-Agent", request.userAgent);因此改动前的行为如下:
项目中也没有统一收集加载器名称、加载器版本和其他运行时版本信息的工具。 具体改动1. 新增
|
| 信息 | 数据来源 |
|---|---|
| CustomSkinLoader 版本 | CustomSkinLoader.CustomSkinLoader_FULL_VERSION |
| Minecraft 版本 | MinecraftUtil.getMinecraftMainVersion() |
| Mod 加载器名称及版本 | 各加载器 Bootstrap 发布的统一运行时属性 |
| JRE 版本 | System.getProperty("java.version") |
使用 CustomSkinLoader_FULL_VERSION 而不是基础版本,是为了让开发构建保留快照和构建编号。例如:
15.0.1-SNAPSHOT-0
正式构建仍会显示正常版本号:
15.0.1
3. 在各加载器 Bootstrap 中读取版本
加载器名称和版本由对应的 Bootstrap 在加载 Common 之前读取:
| 加载器 | Bootstrap 中的读取方式 |
|---|---|
| Fabric / Quilt | 通过 Fabric Loader 公开 API 查询 fabricloader 或 quilt_loader 的 Mod 元数据 |
| Forge | 由当前 Forge Bootstrap 从 Forge/FML 已解析的版本信息中读取 |
| NeoForge | 由当前 NeoForge Bootstrap 从 FML 的 VersionInfo 中读取 |
Fabric Bootstrap 会先查询 quilt_loader。存在该 Mod 容器时报告 Quilt/<版本>,否则查询必然存在的 fabricloader 并报告 Fabric/<版本>。这里不查询 fabric-api,因为 Fabric API 是可选 Mod,不是实际承载游戏的 Mod 加载器。
Bootstrap 目录中的 V1/V2 后缀是项目原有的实现分组,用于适配不同的启动接口和类加载机制,不属于 User Agent 的运行时模型。各 Bootstrap 直接读取当前 Loader 的实际版本,最终只会报告 Forge/<实际版本> 或 NeoForge/<实际版本>;本功能不根据 Minecraft 版本推断 Loader 版本,也不新增 Loader 代际判断。
UserAgentUtil 不再包含任何 Forge、NeoForge、Fabric 或 Quilt 类名,也不再访问 FabricLoaderImpl.VERSION 等加载器内部实现。
4. 将 Bootstrap 信息传递给 Common
新增 Bootstrap/Core/src/main/java/customskinloader/bootstrap/ModLoaderInfo.java,由各 Bootstrap 将检测结果发布到以下命名空间化 JVM 属性:
customskinloader.modLoader.name
customskinloader.modLoader.version
使用 JVM 属性而不是让 Common 直接引用 Bootstrap 静态字段,是因为不同加载器会把 Bootstrap 和释放出的 Common jar 放入不同的类加载器或模块层。系统属性由整个 JVM 共享,可以避免额外的类链接关系,也不会迫使 Bootstrap 提前加载 Common。
UserAgentUtil 只读取这两个属性。当前支持的每一种加载环境都有对应 Bootstrap,名称和版本会在 Common 发出第一个 HTTP 请求前写入。缺少任一属性表示 Bootstrap 初始化协议没有正常完成,此时直接抛出异常,不生成带有 Unknown/unknown 的 User-Agent。
读取成功后的运行时信息缓存在不可变的 RuntimeInfo 中,缓存字段使用 volatile,便于多个皮肤加载线程复用。
5. 统一接入 HTTP 请求
HttpRequestUtil.makeHttpRequest 在建立连接前统一调用:
String userAgent = UserAgentUtil.expandUserAgent(request.userAgent);随后始终设置最终 User-Agent:
c.setRequestProperty("User-Agent", userAgent);将处理放在 HttpRequestUtil 而不是分别修改每个 Profile Loader,可以覆盖现有及未来所有通过该工具发出的请求,并避免不同 Loader 产生不一致行为。
目前 JsonAPILoader 和 LegacyLoader 会将 loadlist 中的 userAgent 传入 HttpRequest,因此这些请求可以展开其中的 CSL 占位符。没有传入自定义值的请求,例如现有 Mojang API 请求,会自动使用完整默认 User-Agent。
6. 添加自定义占位符
loadlist 和 ExtraList 的 userAgent 字段支持以下占位符:
| 占位符 | 替换内容 |
|---|---|
{DEFAULT_USER_AGENT} |
完整的默认 CustomSkinLoader User-Agent |
{CSL_VERSION} |
CustomSkinLoader 完整版本 |
{MINECRAFT_VERSION} |
Minecraft 游戏版本 |
{MOD_LOADER_NAME} |
当前 Mod 加载器名称 |
{MOD_LOADER_VERSION} |
当前 Mod 加载器版本 |
{JAVA_VERSION} |
当前 JRE 版本 |
占位符采用精确的字面量替换:
- 名称区分大小写。
- 占位符名称必须全大写。
- 占位符两边必须使用
{}包裹。 - 只有上表列出的六个完整字符串会被替换,其他内容不参与解析。
例如:
{
"name": "ExampleSkinServer",
"type": "CustomSkinAPI",
"root": "https://example.com/",
"userAgent": "ExampleSkinClient/1.0 (CustomSkinLoader/{CSL_VERSION}; Minecraft/{MINECRAFT_VERSION}; {MOD_LOADER_NAME}/{MOD_LOADER_VERSION}; Java/{JAVA_VERSION})"
}在示例 NeoForge 环境中会展开为:
ExampleSkinClient/1.0 (CustomSkinLoader/15.0.1; Minecraft/26.2; NeoForge/26.2.0.35-beta; Java/25.0.1)
也可以在自定义前缀或后缀中嵌入完整默认值:
{
"userAgent": "ExampleSkinClient/1.0 {DEFAULT_USER_AGENT}"
}7. 规范化自动插入的 Header 字段
Minecraft、加载器、Java 和 CSL 版本来自运行时环境。为了避免空格、分号、换行或其他特殊字符破坏 HTTP Header,自动插入的字段会被规范化为合法的 HTTP token 字符。
例如:
15.0.1 SNAPSHOT -> 15.0.1_SNAPSHOT
26.2 Pre-Release -> 26.2_Pre-Release
Mod 加载器名称和版本是 Bootstrap 必须提供的信息,缺失时请求不会继续发送。Minecraft 版本沿用 MinecraftUtil.getMinecraftMainVersion() 的现有读取及回退行为。该规范化处理仅针对自动插入的组件,不改写自定义 User-Agent 中的其他内容。
改动后的行为
输入的 userAgent |
改动后行为 |
|---|---|
未配置或 null |
生成并发送完整默认 User-Agent |
| 普通固定字符串 | 原样发送,保持现有配置兼容 |
| 包含已知占位符 | 发送前替换为实际运行时信息 |
包含 {DEFAULT_USER_AGENT} |
嵌入完整默认 User-Agent |
| 显式配置空字符串 | 继续发送空值,保持原有显式空字符串语义 |
为什么采用这种实现
在统一 HTTP 入口处理
所有请求最终都经过 HttpRequestUtil。在这里处理默认值和占位符,可以一次覆盖全部请求路径,也能确保未来新增的 Profile Loader 自动获得默认 User-Agent。
由 Bootstrap 负责加载器信息
Bootstrap 本身已经由确定的 Mod Loader 启动,能够使用该加载器的 API 获取版本。把这项职责放在对应 Bootstrap 中,不需要让 UserAgentUtil 猜测当前环境,也不会把加载器专用类名和兼容逻辑扩散到 User-Agent 代码。
各现有 Bootstrap 只处理自己对应的加载器环境。User Agent 代码不新增额外的加载器代际抽象,也不维护 Minecraft 版本到 Bootstrap 类型的映射。
使用 JVM 属性跨越类加载边界
各加载器对 Bootstrap、转换服务和 Common jar 的类加载方式不同。命名空间化 JVM 属性提供了只包含两个字符串的最小通信协议,不要求两个模块共享同一个类实例,也不会引入 Common 到 Bootstrap 的反向依赖。
保留自定义字符串兼容性
展开操作只替换本次定义的六个精确字符串。没有这些字符串的现有 userAgent 会原样发送,不需要用户迁移配置;显式空字符串的行为也没有改变。
将 Loader 信息视为 Bootstrap 协议
当前 Universal 制品没有脱离 Mod Loader 的独立加载入口;Forge Legacy、Forge ModLauncher、NeoForge 和 Fabric 都有对应 Bootstrap,Quilt 通过 Fabric 兼容路径进入 Fabric Bootstrap。Loader 名称和版本是 Bootstrap 与 Common 之间的必需数据;如果任一属性缺失,说明受支持的启动流程已经发生错误。此时明确失败比继续发送不完整或错误的 User-Agent 更便于定位问题。
涉及文件
| 文件 | 改动 |
|---|---|
Bootstrap/Core/src/main/java/customskinloader/bootstrap/ModLoaderInfo.java |
新增加载器信息发布协议 |
Bootstrap/FabricV1/.../TransformerBootstrap.java |
读取并发布 Fabric 或 Quilt 名称和版本 |
Bootstrap/ForgeV1、Bootstrap/ForgeV2 |
由现有 Forge Bootstrap 读取并发布实际版本 |
Bootstrap/NeoForgeV1、Bootstrap/NeoForgeV2 |
由现有 NeoForge Bootstrap 读取并发布实际版本 |
Common/src/main/java/customskinloader/utils/UserAgentUtil.java |
生成 User-Agent、读取 Bootstrap 信息、展开占位符、规范化字段并缓存运行时信息 |
Common/src/main/java/customskinloader/utils/HttpRequestUtil.java |
在统一请求入口解析自定义值并始终设置 User-Agent |
测试与验证
执行完整构建:
./gradlew build验证结果:
- Common 模块构建通过。
- Fabric Bootstrap 构建通过。
- Forge 的现有 Bootstrap 模块构建通过。
- NeoForge 的现有 Bootstrap 模块构建通过。
- 项目没有引入 JUnit 或新增单元测试。
- 最终 Universal jar 包含
ModLoaderInfo及全部加载器的发布调用。 - 最终 Universal jar 的
UserAgentUtil不包含加载器 API 类名或FabricLoaderImpl引用。 - 最终制品包含全部六个公开占位符。
- 最终制品不包含废弃的占位符名称。
git diff --check通过。
兼容性说明
- 未配置
userAgent的请求由“不显式设置”变为“发送完整默认 User-Agent”,这是本次变更的主要行为变化。 - 已配置普通固定 User-Agent 的 loadlist 不需要修改。
- 已配置空字符串的 loadlist 仍然保持空字符串行为。
- 加载器探测由各 Bootstrap 完成,
UserAgentUtil不依赖任何 Mod Loader API。 - Loader 信息缺失时会明确失败,不会发送包含
Unknown/unknown的 User-Agent。 - Fabric 和 Quilt 通过公开 Mod 元数据读取版本,不依赖
FabricLoaderImpl或 jar Manifest。 fabric-api不参与加载器版本检测,未安装 Fabric API 不会影响 User Agent 中 Fabric Loader 版本的读取。- 实现保持 Java 8 源码和字节码兼容要求。
There was a problem hiding this comment.
Pull request overview
Adds a default, runtime-aware User-Agent for HTTP requests, resolving #388.
Changes:
- Builds and expands User-Agent placeholders.
- Publishes loader metadata from each bootstrap.
- Always sends the resolved User-Agent.
Reviewed changes
Copilot reviewed 14 out of 14 changed files in this pull request and generated 2 comments.
Show a summary per file
| File | Description |
|---|---|
Common/.../UserAgentUtil.java |
Builds and expands User-Agents. |
Common/.../MinecraftUtil.java |
Exposes loader properties and removes texture accessors. |
Common/.../HttpRequestUtil.java |
Applies User-Agent to requests. |
Common/.../CustomSkinLoader.java |
Logs loader information. |
Bootstrap/Core/.../ModLoaderInfo.java |
Publishes loader properties. |
Bootstrap/Core/.../ReflectionUtils.java |
Adds cached reflection helpers. |
Bootstrap/FabricV1/.../TransformerBootstrap.java |
Publishes Fabric loader metadata. |
Bootstrap/ForgeV1/.../TransformerBootstrap.java |
Publishes legacy Forge metadata. |
Bootstrap/ForgeV2/.../TransformerBootstrap.java |
Detects Forge/NeoForge versions. |
Bootstrap/ForgeV2/.../TransformationService.java |
Invokes loader detection. |
Bootstrap/ForgeV2/.../CommonModLocator.java |
Reorders imports. |
Bootstrap/NeoForgeV1/.../CommonModLocator.java |
Adds override annotation. |
Bootstrap/NeoForgeV2/.../CommonModLocator.java |
Documents supported versions. |
Bootstrap/NeoForgeV2/.../ClassProcessorProviderImpl.java |
Publishes NeoForge metadata. |
💡 Add a code-review agent skill or configure MCP servers for context-aware, tailored reviews. Learn more in the docs.
| } | ||
|
|
||
| private static void publishModLoaderInfo() { | ||
| ModLoaderInfo.publish("Fabric", FabricLoader.getInstance().getModContainer("fabricloader").map(m -> m.getMetadata().getVersion().getFriendlyString()).orElse("unknown")); |
| System.setProperty(NAME_PROPERTY, name); | ||
| System.setProperty(VERSION_PROPERTY, version); |
This Pull Request resolves #388 .
为 CustomSkinLoader 添加默认 User Agent,详见上述 Issue。
代码由 OpenAI Codex 使用 GPT 5.6 Sol 模型生成,由人类完成测试,不过没测 Quilt,因为 HMCL 的 Quilt 自动安装炸了。
以下是 Codex 生成的改动说明:(点击查看)
变更概述
本次变更为 CustomSkinLoader 的 HTTP 请求添加统一的默认 User-Agent,并允许 loadlist 和 ExtraList 中的自定义
userAgent使用运行时占位符。默认 User-Agent 至少包含以下信息:
默认格式如下:
示例:
同时,自定义 User-Agent 可以使用以下占位符:
变更背景
CustomSkinLoader 会向 Mojang API、第三方皮肤 API 和皮肤纹理地址发起 HTTP 请求。此前这些请求没有统一的默认 User-Agent,第三方皮肤服务端无法稳定获得客户端的 Minecraft、Mod 加载器、Java 和 CustomSkinLoader 版本信息。
loadlist 虽然已经提供
userAgent字段,但该字段只能使用固定字符串。服务端维护者如果希望在 User-Agent 中包含 Minecraft 或加载器版本,只能让用户手动填写,配置无法在不同游戏实例之间复用。本次变更的目标是:
HttpRequestUtil的网络请求拥有可识别的默认 User-Agent。在 Minecraft 26.1.2、Fabric Loader 0.19.2 环境的后续实测中,还发现 Fabric 加载器版本可能回退为
unknown。检查 Fabric Loader 0.19.2 后确认,其 jar Manifest 没有提供可用的Implementation-Version,因此原有的包版本回退路径无法覆盖该情况。本次变更同时补充了适用于 Fabric Loader 0.19.x 的版本兜底。改动前的行为
HttpRequest.userAgent的默认值为null。发送请求时,只有该字段不为null才会调用:因此改动前的行为如下:
userAgentHttpURLConnection实现{MINECRAFT_VERSION}等内容项目中也没有统一收集加载器名称、加载器版本和其他运行时版本信息的工具。
具体改动
1. 新增
UserAgentUtil新增
Common/src/main/java/customskinloader/utils/UserAgentUtil.java,集中负责:将这些逻辑放在独立工具类中,可以保证默认 User-Agent 和自定义占位符始终使用相同的数据来源和格式规则。
2. 生成默认 User-Agent
默认 User-Agent 使用以下运行时数据:
CustomSkinLoader.CustomSkinLoader_FULL_VERSIONMinecraftUtil.getMinecraftMainVersion()System.getProperty("java.version")使用
CustomSkinLoader_FULL_VERSION而不是基础版本,是为了让开发构建保留快照和构建编号。例如:正式构建仍会显示正常版本号:
3. 检测 Mod 加载器及版本
当前支持以下加载器:
现代 NeoForge 和 Forge 优先通过各自的
ModList查找加载器 Mod 容器,并从 Mod 元数据读取实际运行版本。如果 Mod 容器尚未提供版本,还会尝试以下回退接口:
NeoForgeVersionFMLLoader.versionInfo()ForgeVersionFMLLoader.versionInfo()net.minecraftforge.common.ForgeVersionFabric 和 Quilt 分别读取
fabricloader与quilt_loader的 Mod 元数据版本。Fabric 的首选路径在语义上等同于调用以下公开 API:由于该逻辑位于同时供多个加载器使用的 Common 模块中,实际实现通过反射调用这组公开 API,避免 Common 在编译期和类加载期直接依赖 Fabric Loader。
如果公开 Mod 元数据路径没有返回版本,Fabric 检测依次尝试:
net.fabricmc.loader.impl.FabricLoaderImpl.VERSION。FabricLoader所属包的Implementation-Version。unknown。FabricLoaderImpl.VERSION位于 Fabric Loader 的impl包,不属于其稳定公开 API,因此只作为兼容性兜底,不作为首选数据来源。Fabric Loader 0.19.2 的 jar Manifest 没有提供可用的Implementation-Version,只保留包版本回退会导致实测环境仍然得到unknown。这里查询的是 Mod ID
fabricloader,而不是fabric-api。Fabric API 是可选安装的 Mod,不是实际承载游戏的 Mod 加载器;将其发行版本写入{MOD_LOADER_VERSION}会使 User Agent 的含义不正确。所有加载器访问都通过反射完成。Common 模块因此不需要直接链接任意加载器 API,也不会因为缺少其他加载器的类而产生
ClassNotFoundException。加载器检测顺序为:
Forge 和 NeoForge 被放在 Fabric 之前,是为了兼容安装了 Fabric API 兼容层的环境。即使类路径中出现
FabricLoaderAPI,也应优先报告实际承载游戏的 Forge 或 NeoForge。4. 延迟缓存运行时信息
成功读取加载器名称和版本后,结果会缓存在不可变的
RuntimeInfo中。缓存字段使用
volatile,使多个皮肤加载线程能够安全读取已经初始化的结果。如果调用发生得过早,加载器元数据暂时只能返回
unknown,该结果不会被永久缓存。后续真正发送请求时会重新检测,避免启动早期的临时状态影响整个游戏生命周期。5. 统一接入 HTTP 请求
HttpRequestUtil.makeHttpRequest在建立连接前统一调用:随后始终设置最终 User-Agent:
将处理放在
HttpRequestUtil而不是分别修改每个 Profile Loader,可以覆盖现有及未来所有通过该工具发出的请求,并避免不同 Loader 产生不一致行为。目前
JsonAPILoader和LegacyLoader会将 loadlist 中的userAgent传入HttpRequest,因此这些请求可以展开自定义模板。没有传入自定义值的请求,例如现有 Mojang API 请求,会自动使用完整默认 User-Agent。6. 添加自定义占位符
loadlist 和 ExtraList 的
userAgent字段支持以下占位符:{DEFAULT_USER_AGENT}{CSL_VERSION}{MINECRAFT_VERSION}{MOD_LOADER_NAME}{MOD_LOADER_VERSION}{JAVA_VERSION}占位符采用精确的字面量替换,具有以下规则:
{}包裹。例如:
{ "name": "ExampleSkinServer", "type": "CustomSkinAPI", "root": "https://example.com/", "userAgent": "ExampleSkinClient/1.0 (CustomSkinLoader/{CSL_VERSION}; Minecraft/{MINECRAFT_VERSION}; {MOD_LOADER_NAME}/{MOD_LOADER_VERSION}; Java/{JAVA_VERSION})" }在示例 NeoForge 环境中会展开为:
也可以在自定义前缀或后缀中嵌入完整默认值:
{ "userAgent": "ExampleSkinClient/1.0 {DEFAULT_USER_AGENT}" }7. 规范化自动插入的 Header 字段
Minecraft、加载器、Java 和 CSL 版本来自运行时环境。为了避免空格、分号、换行或其他特殊字符破坏 HTTP Header,自动插入的字段会被规范化为合法的 HTTP token 字符。
例如:
无法读取或为空的字段会回退为:
该处理仅针对自动插入的版本组件。用户自定义 User-Agent 模板的其他部分不会被擅自修改。
改动后的行为
userAgentnull{DEFAULT_USER_AGENT}为什么采用这种实现
在统一 HTTP 入口处理
所有请求最终都经过
HttpRequestUtil。在这里处理默认值和占位符,可以一次覆盖全部请求路径,也能确保未来新增的 Profile Loader 自动获得默认 User-Agent。使用反射检测加载器
项目需要同时支持跨度很大的 Minecraft 和加载器版本。Common 模块如果直接依赖 Forge、NeoForge、Fabric 或 Quilt 中的任意一个 API,会在其他环境中产生链接错误。
反射允许代码在运行时仅访问当前实际存在的加载器类,并为不同版本保留多个回退路径。
保留自定义字符串兼容性
没有占位符的现有
userAgent会原样发送,不需要用户迁移配置。显式空字符串的行为也没有改变。未知占位符不报错
保留未知占位符比删除或替换为空字符串更安全。这样既不会损坏用户配置,也允许第三方工具在发送前后继续处理自己的模板字段。
只缓存完整加载器信息
启动早期可能暂时拿不到加载器版本。如果立即缓存
unknown,后续所有请求都会携带错误信息。因此只有名称和版本均可用时才缓存结果。涉及文件
Common/src/main/java/customskinloader/utils/UserAgentUtil.javaCommon/src/main/java/customskinloader/utils/HttpRequestUtil.java测试与验证
执行完整构建:
验证结果:
0.19.2。git diff --check通过。兼容性说明
userAgent的请求由“不显式设置”变为“发送完整默认 User-Agent”,这是本次变更的主要行为变化。Unknown/unknown,不会阻断皮肤加载。FabricLoaderImpl.VERSION仅用于公开路径未返回版本时的兼容性兜底。fabric-api不参与加载器版本检测,未安装 Fabric API 不会影响 User Agent 中 Fabric Loader 版本的读取。