Skip to content

fix(login): 修复正版账号皮肤获取与登录刷新异常 (#2947)#2951

Merged
LuLu-ling merged 1 commit into
PCL-Community:devfrom
AresConnor:fix/ms-account-json-regression
May 31, 2026
Merged

fix(login): 修复正版账号皮肤获取与登录刷新异常 (#2947)#2951
LuLu-ling merged 1 commit into
PCL-Community:devfrom
AresConnor:fix/ms-account-json-regression

Conversation

@AresConnor
Copy link
Copy Markdown
Contributor

@AresConnor AresConnor commented May 30, 2026

关联 Issue

fix #2947:正版账号皮肤无法获取、正版游戏无法登录。

问题原因

两处问题均为 Newtonsoft.JsonSystem.Text.Json 迁移(#2931)后引入的回归。

1. 皮肤无法获取(InvalidCastException

ModNet.NetGetCodeByRequestRetry / NetGetCodeByRequestOnce 中:

return IsJson ? ModBase.GetJson(result) : result;

GetJson 返回 JsonNode,而 resultstringSystem.Text.Json 定义了 stringJsonNode隐式转换,因此编译器将三元表达式的两个分支统一为 JsonNode——即便 IsJson = false,原始字符串也会被包装成 JsonValuePrimitive<string>。调用方 McSkinGetAddress 随后执行 (string)skinString 时便抛出 InvalidCastExceptionNewtonsoft.Json 没有这一隐式转换,故迁移前正常。

对应日志:

[WARN] 获取微软正版皮肤失败(SaltWood_233)
System.InvalidCastException: Unable to cast object of type
'System.Text.Json.Nodes.JsonValuePrimitive`1[System.String]' to type 'System.String'.
   at PCL.ModMinecraft.McSkinGetAddress(String uuid, String type)

2. 正版无法登录(ArgumentNullException

MsLoginStep1Refresh 中刷新令牌请求返回 400 时,response.EnsureSuccessStatusCode() 绑定到了内置方法(实例方法优先于扩展方法),其抛出的异常不包含响应体,导致 invalid_grant / must sign in again 等"令牌失效"判定全部落空。用户点击取消后,代码继续向下执行至 GetJson(Result),而此时 Resultnull,最终抛出令人费解的"格式化 JSON 失败 / Value cannot be null"。

对应日志:

[INFO] 正版验证 Step 1/6 获取 OAuth Token 失败:
       System.Net.Http.HttpRequestException: Response status code does not indicate success: 400 (Bad Request).
...
System.Exception: 登录失败
 ---> System.Exception: 格式化 JSON 失败:
 ---> System.ArgumentNullException: Value cannot be null. (Parameter 'json')
   at PCL.Core.Utils.JsonCompat.ParseNode(String text)

3. Relogin 无限循环(潜在缺陷)

GetOAuthTokensMsLoginStep1Refresh 返回 "Relogin" 时直接 continue,但未清空 input.OAuthRefreshToken,导致再次进入刷新分支,形成无限循环。

修改内容

  • ModNet.cs:将三元表达式的 JSON 分支显式转换为 objectIsJson ? (object)ModBase.GetJson(result) : result),使非 JSON 分支保留原始 string 类型。同时修复 NetGetCodeByRequestRetryNetGetCodeByRequestOnce
  • ModLaunch.cs · MsLoginStep1Refresh:先读取响应体,再抛出携带正文的 HttpRequestException;补充 invalid_grant / must first sign in 检测;用户取消时以 "$$" 静默中止启动,避免落入 GetJson(null) 空引用。
  • ModLaunch.cs · GetOAuthTokensRelogin 时清空 OAuthRefreshToken,回退至设备代码流,避免无限循环。

测试情况

  • dotnet build0 个 C# 编译错误(仅因启动器正在运行导致输出 .exe 被占用的文件锁错误,与本次改动无关)。
  • 受限于需要真实微软账号会话,未能对登录 / 皮肤的实际运行时流程进行端到端测试;上述修复依据 issue 日志中的精确堆栈跟踪定位并验证。

Summary by Sourcery

修复由于 JSON 处理和令牌刷新回归导致的 Microsoft 账号登录和皮肤获取问题。

Bug 修复:

  • 恢复对非 JSON HTTP 响应的正确处理,防止在获取皮肤时因 InvalidCastException 而出错。
  • 在 OAuth 刷新令牌时保留 HTTP 错误响应体,以便正确检测无效或过期的刷新令牌,避免对空结果进行 JSON 解析时出现错误。
  • 通过在重试设备代码流程前清除无效的 OAuth 刷新令牌,防止出现无限重新登录循环。
  • 当用户取消登录时,干净地中止登录流程,避免对空结果进行后续 JSON 解析。

增强:

  • 调整网络辅助工具的返回类型,在禁用 JSON 解析时保留原始字符串响应。

本pr由claude opus 4.8辅助

Original summary in English

Summary by Sourcery

Fix issues with Microsoft account login and skin retrieval caused by JSON handling and token refresh regressions.

Bug Fixes:

  • Restore correct handling of non-JSON HTTP responses to prevent InvalidCastException when retrieving skins.
  • Preserve HTTP error response bodies during OAuth token refresh to correctly detect invalid or expired refresh tokens and avoid null JSON parsing errors.
  • Prevent infinite relogin loops by clearing invalid OAuth refresh tokens before retrying the device code flow.
  • Stop the login flow cleanly when the user cancels, avoiding downstream JSON parsing on null results.

Enhancements:

  • Adjust network helper return types to keep plain string responses intact when JSON parsing is disabled.

@pcl-ce-automation pcl-ce-automation Bot added 🛠️ 等待审查 Pull Request 已完善,等待维护者或负责人进行代码审查 size: S PR 大小评估:小型 labels May 30, 2026
@sourcery-ai
Copy link
Copy Markdown

sourcery-ai Bot commented May 30, 2026

审阅者指南(在小型 PR 上默认折叠)

审阅者指南

修复从 Newtonsoft.Json 迁移到 System.Text.Json 时引入的回归问题,这些问题导致微软账号皮肤获取和基于刷新令牌的登录失效,并通过收紧 JSON 处理、错误传播以及令牌重置逻辑,解决潜在的无限重新登录循环。

皮肤获取 JSON 处理修复的时序图

sequenceDiagram
    participant McSkinGetAddress
    participant ModNet
    participant ModBase

    McSkinGetAddress->>ModNet: NetGetCodeByRequestOnce(url, Encode, Timeout, IsJson=false)
    ModNet->>ModNet: result = Requester.FetchString(url, param)
    ModNet->>ModBase: GetJson(result)
    ModNet-->>McSkinGetAddress: (object)result
    McSkinGetAddress->>McSkinGetAddress: skinString = (string)returnValue
Loading

OAuth 刷新令牌及重新登录处理的时序图

sequenceDiagram
    participant GetOAuthTokens
    participant MsLoginStep1Refresh
    participant Requester
    actor User

    loop login_attempt
        GetOAuthTokens->>MsLoginStep1Refresh: MsLoginStep1Refresh(OAuthRefreshToken)
        MsLoginStep1Refresh->>Requester: Send refresh request
        Requester-->>MsLoginStep1Refresh: response
        MsLoginStep1Refresh->>MsLoginStep1Refresh: Result = response.AsString()
        alt !response.IsSuccess
            MsLoginStep1Refresh-->>GetOAuthTokens: [throws HttpRequestException with body]
        else response.IsSuccess
            MsLoginStep1Refresh-->>GetOAuthTokens: tokens
        end
        alt tokens[0] == Relogin
            GetOAuthTokens->>GetOAuthTokens: OAuthRefreshToken = ""
            GetOAuthTokens-->>GetOAuthTokens: continue loop (device code flow)
        else tokens[0] == Ignore
            GetOAuthTokens-->>GetOAuthTokens: handle ignore
        else user_cancels
            User-->>MsLoginStep1Refresh: cancel login
            MsLoginStep1Refresh-->>GetOAuthTokens: [throws Exception "$$"]
        end
    end
Loading

文件级变更

变更 详情 文件
为网络辅助方法保留正确的返回类型,避免在调用方期望字符串时出现非预期的 JsonNode 包装以及 InvalidCastException。
  • 调整网络获取辅助方法中的三元表达式,使 JSON 分支显式地强制转换为 object,从而防止 System.Text.Json 将普通字符串隐式转换为 JsonNode。
  • 在所有可选解析 JSON 的 HTTP GET 辅助方法中一致应用该修复,包括带重试和单次请求的版本。
Plain Craft Launcher 2/Modules/Network/Facade/ModNet.cs
改进 OAuth 刷新令牌流程,以暴露错误响应体、正确识别无效刷新令牌,并在登录被取消时避免对 null 进行 JSON 解析。
  • 修改 MsLoginStep1Refresh,使其在 HTTP 状态非成功时始终读取响应体,并抛出包含响应体文本的 HttpRequestException,而不是调用 EnsureSuccessStatusCode。
  • 扩展错误信息检查,增加对 invalid_grant 和“必须先登录”类提示的识别,使过期或无效的刷新令牌会触发 Relogin 响应。
  • 当用户在登录界面中途取消时,传播一个哨兵异常("$$"),以提前终止启动流程,防止后续调用 GetJson(null) 并产生令人困惑的 null-JSON 错误。
Plain Craft Launcher 2/Modules/Minecraft/ModLaunch.cs
通过在回退到设备码登录前清除过期的 OAuth 刷新令牌,防止出现无限重新登录循环。
  • 在 GetOAuthTokens 中,当 MsLoginStep1Refresh 返回 Relogin 结果时,在循环继续前将已存储的 OAuthRefreshToken 重置为空字符串,使下一次迭代使用设备码登录流程,而不是再次尝试刷新。
Plain Craft Launcher 2/Modules/Minecraft/ModLaunch.cs

可能关联的问题


提示与命令

与 Sourcery 交互

  • 触发新的审阅: 在 pull request 上评论 @sourcery-ai review
  • 继续讨论: 直接回复 Sourcery 的审阅评论。
  • 从审阅评论生成 GitHub Issue: 在审阅评论下回复,请求 Sourcery 从该评论创建一个 issue。你也可以回复 @sourcery-ai issue 来从该评论生成 issue。
  • 生成 pull request 标题: 在 pull request 标题中任意位置写上 @sourcery-ai 即可随时生成标题。你也可以在 pull request 中评论 @sourcery-ai title 来(重新)生成标题。
  • 生成 pull request 摘要: 在 pull request 正文中任意位置写上 @sourcery-ai summary,即可在该位置生成 PR 摘要。你也可以在 pull request 中评论 @sourcery-ai summary 来(重新)生成摘要。
  • 生成审阅者指南: 在 pull request 中评论 @sourcery-ai guide,即可随时(重新)生成审阅者指南。
  • 解决所有 Sourcery 评论: 在 pull request 中评论 @sourcery-ai resolve,即可标记所有 Sourcery 评论为已解决。如果你已经处理完所有评论且不想再看到它们,这会很有用。
  • 撤销所有 Sourcery 审阅: 在 pull request 中评论 @sourcery-ai dismiss,即可撤销所有现有的 Sourcery 审阅。尤其适用于你想从一次全新的审阅开始——别忘了评论 @sourcery-ai review 来触发新的审阅!

自定义你的使用体验

访问你的 控制面板 来:

  • 启用或禁用审阅功能,例如 Sourcery 生成的 pull request 摘要、审阅者指南等。
  • 更改审阅语言。
  • 添加、删除或编辑自定义审阅说明。
  • 调整其他审阅设置。

获取帮助

Original review guide in English
Reviewer's guide (collapsed on small PRs)

Reviewer's Guide

Fixes regressions introduced by the Newtonsoft.Json → System.Text.Json migration that broke Microsoft account skin retrieval and token refresh-based login, and addresses a potential infinite relogin loop by tightening JSON handling, error propagation, and token reset logic.

Sequence diagram for skin retrieval JSON handling fix

sequenceDiagram
    participant McSkinGetAddress
    participant ModNet
    participant ModBase

    McSkinGetAddress->>ModNet: NetGetCodeByRequestOnce(url, Encode, Timeout, IsJson=false)
    ModNet->>ModNet: result = Requester.FetchString(url, param)
    ModNet->>ModBase: GetJson(result)
    ModNet-->>McSkinGetAddress: (object)result
    McSkinGetAddress->>McSkinGetAddress: skinString = (string)returnValue
Loading

Sequence diagram for OAuth token refresh and relogin handling

sequenceDiagram
    participant GetOAuthTokens
    participant MsLoginStep1Refresh
    participant Requester
    actor User

    loop login_attempt
        GetOAuthTokens->>MsLoginStep1Refresh: MsLoginStep1Refresh(OAuthRefreshToken)
        MsLoginStep1Refresh->>Requester: Send refresh request
        Requester-->>MsLoginStep1Refresh: response
        MsLoginStep1Refresh->>MsLoginStep1Refresh: Result = response.AsString()
        alt !response.IsSuccess
            MsLoginStep1Refresh-->>GetOAuthTokens: [throws HttpRequestException with body]
        else response.IsSuccess
            MsLoginStep1Refresh-->>GetOAuthTokens: tokens
        end
        alt tokens[0] == Relogin
            GetOAuthTokens->>GetOAuthTokens: OAuthRefreshToken = ""
            GetOAuthTokens-->>GetOAuthTokens: continue loop (device code flow)
        else tokens[0] == Ignore
            GetOAuthTokens-->>GetOAuthTokens: handle ignore
        else user_cancels
            User-->>MsLoginStep1Refresh: cancel login
            MsLoginStep1Refresh-->>GetOAuthTokens: [throws Exception "$$"]
        end
    end
Loading

File-Level Changes

Change Details Files
Preserve correct return types for network helpers to avoid unintended JsonNode wrapping and InvalidCastException when consumers expect strings.
  • Adjust the ternary expression in the network fetch helpers so the JSON branch is explicitly cast to object, preventing System.Text.Json from implicitly converting plain strings to JsonNode.
  • Apply the same fix consistently to both retrying and single-shot HTTP GET helpers that optionally parse JSON.
Plain Craft Launcher 2/Modules/Network/Facade/ModNet.cs
Improve OAuth token refresh flow to surface error bodies, correctly recognize invalid refresh tokens, and avoid null JSON parsing when login is cancelled.
  • Change MsLoginStep1Refresh to always read the HTTP response body and throw an HttpRequestException that includes the body text when the status is non-success, instead of calling EnsureSuccessStatusCode.
  • Expand error message checks to include invalid_grant and must first sign in indicators so expired or invalid refresh tokens trigger a Relogin response.
  • When the user cancels during the login UI, propagate a sentinel exception ("$$") to stop startup early, preventing subsequent GetJson(null) calls and confusing null-JSON errors.
Plain Craft Launcher 2/Modules/Minecraft/ModLaunch.cs
Prevent infinite relogin loops by clearing stale OAuth refresh tokens before falling back to device-code login.
  • In GetOAuthTokens, when MsLoginStep1Refresh returns a Relogin result, reset the stored OAuthRefreshToken to an empty string before looping so that the next iteration uses the device-code flow instead of attempting refresh again.
Plain Craft Launcher 2/Modules/Minecraft/ModLaunch.cs

Possibly linked issues


Tips and commands

Interacting with Sourcery

  • Trigger a new review: Comment @sourcery-ai review on the pull request.
  • Continue discussions: Reply directly to Sourcery's review comments.
  • Generate a GitHub issue from a review comment: Ask Sourcery to create an
    issue from a review comment by replying to it. You can also reply to a
    review comment with @sourcery-ai issue to create an issue from it.
  • Generate a pull request title: Write @sourcery-ai anywhere in the pull
    request title to generate a title at any time. You can also comment
    @sourcery-ai title on the pull request to (re-)generate the title at any time.
  • Generate a pull request summary: Write @sourcery-ai summary anywhere in
    the pull request body to generate a PR summary at any time exactly where you
    want it. You can also comment @sourcery-ai summary on the pull request to
    (re-)generate the summary at any time.
  • Generate reviewer's guide: Comment @sourcery-ai guide on the pull
    request to (re-)generate the reviewer's guide at any time.
  • Resolve all Sourcery comments: Comment @sourcery-ai resolve on the
    pull request to resolve all Sourcery comments. Useful if you've already
    addressed all the comments and don't want to see them anymore.
  • Dismiss all Sourcery reviews: Comment @sourcery-ai dismiss on the pull
    request to dismiss all existing Sourcery reviews. Especially useful if you
    want to start fresh with a new review - don't forget to comment
    @sourcery-ai review to trigger a new review!

Customizing Your Experience

Access your dashboard to:

  • Enable or disable review features such as the Sourcery-generated pull request
    summary, the reviewer's guide, and others.
  • Change the review language.
  • Add, remove or edit custom review instructions.
  • Adjust other review settings.

Getting Help

Copy link
Copy Markdown

@sourcery-ai sourcery-ai Bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Hey - 我在这里提供了一些总体反馈:

  • 使用 throw new Exception("$$") 作为控制流信号既脆弱又不清晰;建议引入一个专门的异常类型,或者使用一个命名清晰的常量 / 枚举 / 结果码,这样调用方就可以可靠地区分这条执行路径,而不必依赖魔法字符串。
  • MsLoginStep1Refresh 中的错误分类依赖于对本地化 / 远程错误消息的子串检查(例如 invalid_grantmust first sign in);如果可能,优先检查响应 JSON 中的结构化字段,比如状态码或错误码,以便让这段逻辑在错误消息文本变更时更加健壮。
给 AI 代理的提示
Please address the comments from this code review:

## Overall Comments
- Using `throw new Exception("$$")` as a control-flow signal is brittle and unclear; consider introducing a dedicated exception type or a well-named constant/enum/result code so callers can reliably distinguish this path without depending on a magic string.
- The error classification in `MsLoginStep1Refresh` relies on substring checks of localized/remote error messages (e.g., `invalid_grant`, `must first sign in`); if possible, prefer checking structured fields such as status codes or error codes from the response JSON to make the logic more robust to message text changes.

Sourcery 对开源项目是免费的——如果你觉得我们的评审有帮助,欢迎分享 ✨
帮我变得更有用!请在每条评论上点 👍 或 👎,我会根据你的反馈改进后续的评审。
Original comment in English

Hey - I've left some high level feedback:

  • Using throw new Exception("$$") as a control-flow signal is brittle and unclear; consider introducing a dedicated exception type or a well-named constant/enum/result code so callers can reliably distinguish this path without depending on a magic string.
  • The error classification in MsLoginStep1Refresh relies on substring checks of localized/remote error messages (e.g., invalid_grant, must first sign in); if possible, prefer checking structured fields such as status codes or error codes from the response JSON to make the logic more robust to message text changes.
Prompt for AI Agents
Please address the comments from this code review:

## Overall Comments
- Using `throw new Exception("$$")` as a control-flow signal is brittle and unclear; consider introducing a dedicated exception type or a well-named constant/enum/result code so callers can reliably distinguish this path without depending on a magic string.
- The error classification in `MsLoginStep1Refresh` relies on substring checks of localized/remote error messages (e.g., `invalid_grant`, `must first sign in`); if possible, prefer checking structured fields such as status codes or error codes from the response JSON to make the logic more robust to message text changes.

Sourcery is free for open source - if you like our reviews please consider sharing them ✨
Help me be more useful! Please click 👍 or 👎 on each comment and I'll use the feedback to improve your reviews.

@AresConnor AresConnor requested a review from SALTWOOD May 30, 2026 16:21
@Pigeon0v0
Copy link
Copy Markdown
Contributor

如果是由 AI 辅助的 PR,记得在 Description 里标注一下模型名称

@AresConnor
Copy link
Copy Markdown
Contributor Author

如果是由 AI 辅助的 PR,记得在 Description 里标注一下模型名称

收到,已标注名称
辅助模型:claude opus 4.8

SALTWOOD
SALTWOOD previously approved these changes May 31, 2026
Copy link
Copy Markdown
Member

@SALTWOOD SALTWOOD left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

LGTM?

lhx077
lhx077 previously approved these changes May 31, 2026
@LuLu-ling
Copy link
Copy Markdown
Member

conflicts

@AresConnor AresConnor dismissed stale reviews from lhx077 and SALTWOOD via 71422de May 31, 2026 11:33
@AresConnor AresConnor force-pushed the fix/ms-account-json-regression branch from 7b0275a to 71422de Compare May 31, 2026 11:33
@pcl-ce-automation pcl-ce-automation Bot added 🕑 等待合并 已处理完毕,正在等待代码合并入主分支 and removed 🛠️ 等待审查 Pull Request 已完善,等待维护者或负责人进行代码审查 labels May 31, 2026
System.Text.Json 迁移后引入的两处回归:

- ModNet.NetGetCodeByRequestRetry/Once 中三元表达式因 string→JsonNode
  隐式转换被统一为 JsonNode,导致非 JSON 分支也被包装成
  JsonValuePrimitive<string>,皮肤解析处 (string) 强转抛
  InvalidCastException。将 JSON 分支显式转为 object 以保留各分支原类型。
- MsLoginStep1Refresh 中 EnsureSuccessStatusCode 绑定到内置方法,抛出的
  异常不含响应体,使 invalid_grant 等失效令牌检测失效;用户取消后落入
  GetJson(null) 抛出空引用。改为先读取响应体再抛出携带正文的异常,
  补充 invalid_grant 检测,并在取消时以 "$$" 静默中止。
- GetOAuthTokens 在 Relogin 时未清空 OAuthRefreshToken 导致无限循环,
  现清空后回退至设备代码流。
@AresConnor AresConnor force-pushed the fix/ms-account-json-regression branch from 71422de to df430c3 Compare May 31, 2026 11:45
@pcl-ce-automation pcl-ce-automation Bot added 🛠️ 等待审查 Pull Request 已完善,等待维护者或负责人进行代码审查 and removed 🕑 等待合并 已处理完毕,正在等待代码合并入主分支 labels May 31, 2026
@AresConnor
Copy link
Copy Markdown
Contributor Author

终于签上了喵

@LuLu-ling LuLu-ling merged commit e99b8fc into PCL-Community:dev May 31, 2026
3 checks passed
@pcl-ce-automation pcl-ce-automation Bot added 👌 完成 相关问题已修复或功能已实现,计划在下次版本更新时正式上线 and removed 🛠️ 等待审查 Pull Request 已完善,等待维护者或负责人进行代码审查 labels May 31, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

size: S PR 大小评估:小型 👌 完成 相关问题已修复或功能已实现,计划在下次版本更新时正式上线

Projects

None yet

Development

Successfully merging this pull request may close these issues.

[BUG] 正版账号信息获取异常

5 participants