Skip to content

refactor(http): 重构 HTTP 请求的创建和处理方式#2591

Merged
tangge233 merged 12 commits intodevfrom
refactor/request
Mar 18, 2026
Merged

refactor(http): 重构 HTTP 请求的创建和处理方式#2591
tangge233 merged 12 commits intodevfrom
refactor/request

Conversation

@tangge233
Copy link
Copy Markdown
Contributor

@tangge233 tangge233 commented Mar 11, 2026

Summary by Sourcery

在整个代码库中重构 HTTP 请求/响应处理逻辑,改为使用新的流式(fluent)辅助 API,同时改进重试行为、错误处理,以及内部 HTTP 服务器命名空间。

Bug 修复:

  • 修复 NeoForge 列表加载逻辑,向 DlSourceLoader 传入正确的加载器实例,确保下载源选择能正确反映调用方的状态。

增强与改进:

  • 引入新的 HttpRequest 辅助类和基于扩展方法的流式 API,用于构建、发送和处理 HTTP 请求与响应,替代之前的 HttpRequestBuilder/HttpResponseHandler 工具。
  • 将所有相关的认证、下载、遥测、更新以及 UI 组件迁移为使用新的 HTTP 辅助 API,包括 Yggdrasil、OAuth、OpenId 和 Natayark 等共享工具的集成部分。
  • 改进 HTTP 错误处理,新增诸如 EnsureSuccessStatusCode 等便捷方法、统一的 JSON 反序列化辅助工具、更丰富的超时处理,以及一致的请求头/Cookie 工具方法。
  • 调整 NetworkService 的生命周期集成和重试策略,使用声明式生命周期特性(attributes)以及带有上限的指数退避(exponential backoff)策略。
  • 重命名并迁移内部 HTTP 服务器类型(HttpServerHttpRouteHttpRouteResponse),将其整合到统一的 PCL.Core.IO.Net.Http 命名空间中,以获得更清晰的组织结构和更好的复用性。
Original summary in English

Summary by Sourcery

Refactor HTTP request/response handling to use a new fluent helper API across the codebase, while improving retry behavior, error handling, and internal HTTP server namespaces.

Bug Fixes:

  • Fix NeoForge list loading to pass the correct loader instance into DlSourceLoader, ensuring download source selection respects the caller's state.

Enhancements:

  • Introduce a new HttpRequest helper and extension-based fluent API for building, sending, and consuming HTTP requests and responses, replacing the previous HttpRequestBuilder/HttpResponseHandler utilities.
  • Update all relevant authentication, download, telemetry, update, and UI components to use the new HTTP helper API, including shared utilities like Yggdrasil, OAuth, OpenId, and Natayark integrations.
  • Improve HTTP error handling by adding convenience methods such as EnsureSuccessStatusCode, unified JSON deserialization helpers, richer timeout handling, and consistent header/cookie utilities.
  • Adjust NetworkService lifecycle integration and retry policy to use declarative lifecycle attributes and an exponential backoff strategy with capped delays.
  • Rename and relocate internal HTTP server types (HttpServer, HttpRoute, HttpRouteResponse) into a consolidated PCL.Core.IO.Net.Http namespace for clearer organization and reuse.

@pcl-ce-automation pcl-ce-automation bot added 🚧 正在处理 开发人员正在对该内容进行开发、测试或修复,进展中 size: XL PR 大小评估:超大型 labels Mar 11, 2026
@pcl-ce-automation pcl-ce-automation bot added size: XXL PR 大小评估:巨型 and removed size: XL PR 大小评估:超大型 labels Mar 13, 2026
@tangge233 tangge233 marked this pull request as ready for review March 13, 2026 09:23
@pcl-ce-automation pcl-ce-automation bot added 🛠️ 等待审查 Pull Request 已完善,等待维护者或负责人进行代码审查 and removed 🚧 正在处理 开发人员正在对该内容进行开发、测试或修复,进展中 labels Mar 13, 2026
@LinQingYuu LinQingYuu requested a review from a team March 13, 2026 10:14
Hill23333
Hill23333 previously approved these changes Mar 13, 2026
Copy link
Copy Markdown
Contributor

@Hill23333 Hill23333 left a comment

Choose a reason for hiding this comment

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

没看

Copy link
Copy Markdown
Contributor

@ruattd ruattd left a comment

Choose a reason for hiding this comment

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

我稍微复杂点的网络请求居然还需要写两行 using!

另外,那巨量更改咱要不留个兼容层等以后慢慢改吧,或者 CSharpE 就直接丢了,真没人乐意同步...

@tangge233
Copy link
Copy Markdown
Contributor Author

我稍微复杂点的网络请求居然还需要写两行 using!

🤔暂时没想出第二行 using 在哪里。目前只要 using PCL.Core.IO.Net.Http.Client.Request; 就能用

@lhx077

This comment was marked as resolved.

lhx077

This comment was marked as resolved.

@lhx077
Copy link
Copy Markdown
Member

lhx077 commented Mar 15, 2026

有一些问题:
第一个是value 允许为空时要不要写 Cookie?目前既有 ThrowIfNullOrEmpty ,后面又有 _GetSafeCookieValue 对空直接 return,感觉有点奇怪

还有一个就是连续两次 ThrowIfNull(value),而 name 未校验,这个是不是应该做修改

@ruattd
Copy link
Copy Markdown
Contributor

ruattd commented Mar 16, 2026

🤔暂时没想出第二行 using 在哪里。目前只要 using PCL.Core.IO.Net.Http.Client.Request; 就能用

看错了,那俩上层的更改是删除

于是现在变成了另一个问题:上一层还有什么用?
以及可以考虑把 Client 和 Server 丢在一起,因为类名本身已经有很明显的区分了,再多分一层命名空间有点多余

@tangge233
Copy link
Copy Markdown
Contributor Author

@sourcery-ai review

@sourcery-ai
Copy link
Copy Markdown

sourcery-ai bot commented Mar 16, 2026

Reviewer's Guide

在整个代码库中重构 HTTP 请求/响应处理,统一使用新的 HttpRequest + 基于扩展方法的 API;更新 NetworkService 的生命周期和重试策略;调整服务端 HTTP 辅助工具和调用方,使网络行为更清晰、更安全、更一致。

新 HTTP 请求/响应辅助 API 的类图

classDiagram
    direction LR

    class HttpRequest {
        <<static>>
        +HttpRequestMessage Create(url)
        +HttpRequestMessage CreateHead(url)
        +HttpRequestMessage CreatePost(url)
        +HttpRequestMessage CreatePut(url)
        +HttpRequestMessage CreateDelete(url)
        +Task~string~ GetStringAsync(url)
        +Task~T~ GetJsonAsync~T~(url)
        +Task~HttpResponseMessage~ PostJsonAsync~T~(url, data, contentType)
    }

    class HttpSenderExtension {
        <<static, extension(HttpRequestMessage)>>
        +Task~HttpResponseMessage~ SendAsync(httpClient, addMetedata, enableLogging, httpCompletionOption, retryTimes, cancellationToken)
    }

    class HttpResponseExtension {
        <<static, extension(HttpResponseMessage)>>
        +bool IsSuccess
        +string AsString()
        +Task~string~ AsStringAsync(ct)
        +Task~Stream~ AsStreamAsync(ct)
        +Task~byte[]~ AsByteArrayAsync(ct)
        +Task~T~ AsJsonAsync~T~(options, cancellationToken)
        +Dictionary~string,string[]~ GetHeaders()
        +Dictionary~string,string[]~ GetContentHeaders()
        +string[] GetHeader(name)
        +bool TryGetHeader(name, out values)
        +string GetFirstHeaderValue(name)
        +bool TryGetFirstHeaderValue(name, out value)
        +void EnsureSuccessStatusCode()
        +Task EnsureSuccessStatusCodeWithContentAsync(ct)
    }

    class HttpContentExtension {
        <<static, extension(HttpRequestMessage)>>
        +HttpRequestMessage WithContent(HttpContent, contentType)
        +HttpRequestMessage WithContent(string, contentType)
        +HttpRequestMessage WithBinaryContent(byte[])
        +HttpRequestMessage WithJsonContent~T~(content, contentType)
        +HttpRequestMessage WithFormContent(string)
        +HttpRequestMessage WithFormContent(IEnumerable~KeyValuePair~)
    }

    class HttpHeaderHandler {
        <<static, extension(HttpRequestMessage)>>
        +HttpRequestMessage WithHeader(key, value)
        +HttpRequestMessage WithHeaders(IDictionary~string,string~)
        +HttpRequestMessage WithHeader(KeyValuePair~string,string~)
        +HttpRequestMessage WithAuthentication(scheme, token)
        +HttpRequestMessage WithBearerToken(token)
    }

    class HttpCookieExtension {
        <<static, extension(HttpRequestMessage)>>
        +HttpRequestMessage WithCookie(name, value)
        -string _GetSafeCookieValue(value)
        -char[] _ForbiddenCookieValueChar
    }

    class HttpBasicExtension {
        <<static, extension(HttpRequestMessage)>>
        +HttpRequestMessage WithHttpVersionOption(httpVersion)
    }

    class NetworkService {
        <<partial, LifecycleScope(network)>>
        -static ServiceProvider _provider
        -static IHttpClientFactory _factory
        -const int BaseRetryDelayMs
        -const int MaxRetryDelayMs
        +static HttpClient GetClient(wantClientType)
        +static AsyncPolicy GetRetryPolicy(retry, retryPolicy)
        -static void _Start()
        -static void _Stop()
        -static TimeSpan _DefaultSleepDurationProvider(attempt)
    }

    class LogWrapper {
        +static void Info(category, message)
        +static void Debug(category, message)
        +static void Error(exception, message)
        +static void Error(exception, message, detail)
    }

    class Basics {
        +static string VersionName
        +static string VersionCode
    }

    HttpRequest ..> HttpSenderExtension : uses SendAsync()
    HttpRequest ..> HttpContentExtension : uses WithJsonContent()
    HttpRequest ..> HttpResponseExtension : uses AsStringAsync(), AsJsonAsync()

    HttpSenderExtension ..> NetworkService : uses GetClient(), GetRetryPolicy()
    HttpSenderExtension ..> LogWrapper : logging
    HttpSenderExtension ..> Basics : request metadata

    HttpContentExtension ..> HttpHeaderHandler : WithHeader()
    HttpHeaderHandler ..> HttpRequestMessage : modifies
    HttpCookieExtension ..> HttpRequestMessage : modifies
    HttpBasicExtension ..> HttpRequestMessage : modifies

    HttpResponseExtension ..> HttpResponseMessage : extends
Loading

File-Level Changes

Change Details Files
引入新的流式(fluent)HTTP 客户端辅助 API,并弃用之前的 HttpRequestBuilder/HttpResponseHandler 抽象。
  • 新增 HttpRequest 工厂,提供 GET/HEAD/POST/PUT/DELETE 的便捷方法,以及高层级的 GetStringAsync/GetJsonAsync/PostJsonAsync 方法。
  • HttpRequestMessage 上新增基于扩展方法的辅助函数,用于设置 headers、cookies、content、HTTP 版本,以及带重试、日志和默认元数据的发送能力。
  • HttpResponseMessage 上新增扩展方法,用于读取内容(string/stream/bytes/JSON)、访问 header,以及校验成功状态的方法,包括 EnsureSuccessStatusCodeWithContentAsync
  • 移除旧的 HttpRequestBuilderHttpResponseHandler 类型,改用新的基于扩展方法的模型。
PCL.Core/IO/Net/Http/HttpRequest.cs
PCL.Core/IO/Net/Http/HttpSenderExtension.cs
PCL.Core/IO/Net/Http/HttpContentExtension.cs
PCL.Core/IO/Net/Http/HttpHeaderExtension.cs
PCL.Core/IO/Net/Http/HttpCookieExtension.cs
PCL.Core/IO/Net/Http/HttpBasicExtension.cs
PCL.Core/IO/Net/Http/HttpResponseExtension.cs
PCL.Core/IO/Net/Http/Client/HttpRequestBuilder.cs
PCL.Core/IO/Net/Http/Client/HttpResponseHandler.cs
将认证、身份和 Yggdrasil 相关的 HTTP 调用迁移到新的带 JSON 辅助方法的 HttpRequest API。
  • 重构 YggdrasilLegacyClient 中的方法,使用 HttpRequest.CreatePost 构建 POST 请求、应用 headers、通过 options 中提供的 HttpClient 发送,并通过 AsJsonAsync<T> 反序列化响应。
  • 重构 OAuth 客户端方法(授权、设备码、刷新),用 HttpRequest + WithContent/WithHeaders 构建请求,并使用 AsJsonAsync<T> 解析响应。
  • 更新 OpenIdOptionsYggdrasilOptions 的初始化和密钥获取逻辑,改为使用 HttpRequest.Create + WithHeaders + AsJsonAsync,替代手动构建 HttpRequestMessage 和直接使用 JsonSerializer
  • 调整 ApiLocation.TryRequestAsync,改用 HttpRequest.CreateHead 和新的 header 访问辅助方法(TryGetHeader)处理 X-Authlib-Injector-Api-Location
PCL.Core/Minecraft/IdentityModel/Yggdrasil/Client.cs
PCL.Core/Minecraft/IdentityModel/OAuth/Client.cs
PCL.Core/Minecraft/IdentityModel/Extensions/OpenId/OpenIdOptions.cs
PCL.Core/Minecraft/IdentityModel/Extensions/YggdrasilConnect/YggdrasilOptions.cs
PCL.Core/Minecraft/Yggdrasil/ApiLocation.cs
更新启动器的 VB 模块和页面以使用新的 HTTP 辅助方法,增加显式成功检查,并简化响应处理。
  • ModLaunch.vb 中,将 HttpRequestBuilder 使用替换为 HttpRequest.* 来处理 Microsoft/Xbox/Minecraft 认证流程,改用 WithFormContent/WithJsonContentSendAsyncEnsureSuccessStatusCodeAsString
  • 更新 ModDownload.vbModDownloadLib.vb,使用 HttpRequest.CreateSendAsyncEnsureSuccessStatusCodeAsString 获取 OptiFine 及其他元数据,同时包含一些空白和变量的小修正。
  • 调整多个与更新相关的模型(UpdatesMinioModelUpdatesMirrorChyanModelUpdatesHomepageMarket),统一改为调用 HttpRequest.Create().SendAsync(),并通过 AsString/AsJsonAsync 读取内容,在合适的位置增加 EnsureSuccessStatusCode
  • 重构 PageLoginAuthMyImagePageSetupAboutNewsViewModel,统一使用 HttpRequest.CreateSendAsyncAsString/AsJsonAsync,用 IsSuccessStatusCode 替代 IsSuccess,并在需要时禁用默认 headers。
Plain Craft Launcher 2/Modules/Minecraft/ModLaunch.vb
Plain Craft Launcher 2/Modules/Minecraft/ModDownload.vb
Plain Craft Launcher 2/Modules/PageDownload/ModDownloadLib.vb
Plain Craft Launcher 2/Modules/Updates/UpdatesMinioModel.vb
Plain Craft Launcher 2/Modules/Updates/UpdatesMirrorChyanModel.vb
Plain Craft Launcher 2/Pages/PageLaunch/PageLoginAuth.xaml.vb
Plain Craft Launcher 2/Controls/MyImage.vb
Plain Craft Launcher 2/Pages/PageSetup/PageSetupAbout.xaml.vb
Plain Craft Launcher 2/Pages/PageSetup/PageHomePageMarket.xaml.vb
PCL.Core/ViewModel/Homepage/NewsViewModel.cs
重构 NetworkService 的生命周期集成和 HTTP 重试策略,并更新依赖其行为的代码。
  • 用基于属性的生命周期([LifecycleService][LifecycleScope])替代 GeneralService 继承,并将静态 start/stop 方法标记为 [LifecycleStart]/[LifecycleStop]
  • NetworkService 中为默认 HttpClient 配置优化后的 SocketsHttpHandler 设置(代理、解压、DoH 连接回调,将 MaxAutomaticRedirections 降至 20)。
  • 重新定义重试退避逻辑,使用带可配置基础/最大延迟常量的指数策略,并修改 GetRetryPolicy 使用同步的 onRetry 日志器,输出更丰富的日志信息。
  • 更新 HttpSenderExtension.SendAsync 以依赖 NetworkService.GetRetryPolicy,在每次尝试时克隆请求,并使用关联 ID 记录每个请求的生命周期和重试尝试。
PCL.Core/IO/Net/NetworkService.cs
PCL.Core/IO/Net/Http/HttpSenderExtension.cs
调整内部 HTTP 服务器类型的命名空间以及对应引用。
  • HttpRouteHttpRouteResponseHttpServerPCL.Core.IO.Net.Http.Server 命名空间移动到 PCL.Core.IO.Net.Http
  • 更新测试和 VB 模块(如 ModWebServerWebServerTest)中的 using/import 语句以引用新的命名空间。
PCL.Core/IO/Net/Http/HttpRoute.cs
PCL.Core/IO/Net/Http/HttpRouteResponse.cs
PCL.Core/IO/Net/Http/HttpServer.cs
PCL.Core.Test/Network/WebServerTest.cs
Plain Craft Launcher 2/Modules/ModWebServer.vb
使外部集成模块(Natayark、EasyTier、Lobby、Telemetry)与新的 HTTP 抽象和语义保持一致。
  • 重构 NatayarkProfileManager,使用 HttpRequest.Create/CreatePostWithContent/WithBearerTokenSendAsyncEnsureSuccessStatusCodeAsStringAsync,并收紧对响应的空值检查。
  • 将 EasyTier 公共节点获取逻辑中自定义的 Polly 重试逻辑替换为简单的 HttpRequest.Create().SendAsync().EnsureSuccessStatusCode() 并通过 AsJsonAsync 进行 JSON 解析。
  • 更新 LobbyController 的遥测 POST,改为使用 HttpRequest.CreatePost + WithContent + WithBearerToken + SendAsync,并调整成功检查逻辑以依赖新的响应辅助属性。
  • 修改 TelemetryService,通过 HttpRequest.CreatePost + WithHeader("Authorization", key) + WithJsonContent + SendAsync 发送环境数据,同时继续使用 HttpResponseExtension 中的 IsSuccess 辅助属性。
PCL.Core/Link/Natayark/NatayarkProfileManager.cs
PCL.Core/Link/Scaffolding/EasyTier/EasyTierEntity.cs
PCL.Core/Link/Lobby/LobbyController.cs
PCL.Core/App/Essentials/TelemetryService.cs
应用在重构过程中发现的一些小的正确性和风格修复。
  • 修复 DlNeoForgeListMain,使其始终使用本地的 loader 参数而不是 Loader,并从正确的实例上传递 IsForceRestarting
  • 将若干 VB 和 C# 文件中的导入从 PCL.Core.IO.Net.Http.Client 规范为 PCL.Core.IO.Net.Http.Client.Request,以使用新的辅助命名空间。
  • 整理若干轻微的格式问题(空白、花括号、请求初始化风格),并确保在新调用点在处理响应之前调用 EnsureSuccessStatusCode
  • 在略微调整 HttpRequestMessage 初始化风格的同时,保持 DoH 连接单元测试逻辑的一致性。
Plain Craft Launcher 2/Modules/Minecraft/ModDownload.vb
Plain Craft Launcher 2/Pages/PageLaunch/PageLoginAuth.xaml.vb
Plain Craft Launcher 2/Modules/Minecraft/ModLaunch.vb
PCL.Core.Test/Network/DohConnection.cs
Plain Craft Launcher 2/Modules/Updates/UpdatesMinioModel.vb
Plain Craft Launcher 2/Modules/Updates/UpdatesMirrorChyanModel.vb
Plain Craft Launcher 2/Controls/MyImage.vb
Plain Craft Launcher 2/Modules/ModWebServer.vb
PCL.Core/Link/Lobby/LobbyController.cs
PCL.Core/App/Essentials/TelemetryService.cs

Tips and commands

Interacting with 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 评论标记为已解决。适用于你已经解决所有评论且不希望再看到它们的情况。
  • 一次性 dismiss 所有 Sourcery 审查: 在 pull request 中评论 @sourcery-ai dismiss,dismiss 所有已有的 Sourcery 审查。特别适合当你想从一次全新的审查开始时——别忘了再评论 @sourcery-ai review 以触发新的审查!

Customizing Your Experience

访问你的 dashboard 以:

  • 启用或禁用审查功能,例如 Sourcery 生成的 pull request 总结、审查者指南等。
  • 更改审查语言。
  • 添加、移除或编辑自定义审查指令。
  • 调整其他审查设置。

Getting Help

Original review guide in English

Reviewer's Guide

Refactors HTTP request/response handling across the codebase to use a new HttpRequest + extension-based API, updates NetworkService lifecycle and retry policy, and adjusts server-side HTTP helpers and call sites for clearer, safer, and more consistent networking behavior.

Class diagram for new HTTP request/response helper API

classDiagram
    direction LR

    class HttpRequest {
        <<static>>
        +HttpRequestMessage Create(url)
        +HttpRequestMessage CreateHead(url)
        +HttpRequestMessage CreatePost(url)
        +HttpRequestMessage CreatePut(url)
        +HttpRequestMessage CreateDelete(url)
        +Task~string~ GetStringAsync(url)
        +Task~T~ GetJsonAsync~T~(url)
        +Task~HttpResponseMessage~ PostJsonAsync~T~(url, data, contentType)
    }

    class HttpSenderExtension {
        <<static, extension(HttpRequestMessage)>>
        +Task~HttpResponseMessage~ SendAsync(httpClient, addMetedata, enableLogging, httpCompletionOption, retryTimes, cancellationToken)
    }

    class HttpResponseExtension {
        <<static, extension(HttpResponseMessage)>>
        +bool IsSuccess
        +string AsString()
        +Task~string~ AsStringAsync(ct)
        +Task~Stream~ AsStreamAsync(ct)
        +Task~byte[]~ AsByteArrayAsync(ct)
        +Task~T~ AsJsonAsync~T~(options, cancellationToken)
        +Dictionary~string,string[]~ GetHeaders()
        +Dictionary~string,string[]~ GetContentHeaders()
        +string[] GetHeader(name)
        +bool TryGetHeader(name, out values)
        +string GetFirstHeaderValue(name)
        +bool TryGetFirstHeaderValue(name, out value)
        +void EnsureSuccessStatusCode()
        +Task EnsureSuccessStatusCodeWithContentAsync(ct)
    }

    class HttpContentExtension {
        <<static, extension(HttpRequestMessage)>>
        +HttpRequestMessage WithContent(HttpContent, contentType)
        +HttpRequestMessage WithContent(string, contentType)
        +HttpRequestMessage WithBinaryContent(byte[])
        +HttpRequestMessage WithJsonContent~T~(content, contentType)
        +HttpRequestMessage WithFormContent(string)
        +HttpRequestMessage WithFormContent(IEnumerable~KeyValuePair~)
    }

    class HttpHeaderHandler {
        <<static, extension(HttpRequestMessage)>>
        +HttpRequestMessage WithHeader(key, value)
        +HttpRequestMessage WithHeaders(IDictionary~string,string~)
        +HttpRequestMessage WithHeader(KeyValuePair~string,string~)
        +HttpRequestMessage WithAuthentication(scheme, token)
        +HttpRequestMessage WithBearerToken(token)
    }

    class HttpCookieExtension {
        <<static, extension(HttpRequestMessage)>>
        +HttpRequestMessage WithCookie(name, value)
        -string _GetSafeCookieValue(value)
        -char[] _ForbiddenCookieValueChar
    }

    class HttpBasicExtension {
        <<static, extension(HttpRequestMessage)>>
        +HttpRequestMessage WithHttpVersionOption(httpVersion)
    }

    class NetworkService {
        <<partial, LifecycleScope(network)>>
        -static ServiceProvider _provider
        -static IHttpClientFactory _factory
        -const int BaseRetryDelayMs
        -const int MaxRetryDelayMs
        +static HttpClient GetClient(wantClientType)
        +static AsyncPolicy GetRetryPolicy(retry, retryPolicy)
        -static void _Start()
        -static void _Stop()
        -static TimeSpan _DefaultSleepDurationProvider(attempt)
    }

    class LogWrapper {
        +static void Info(category, message)
        +static void Debug(category, message)
        +static void Error(exception, message)
        +static void Error(exception, message, detail)
    }

    class Basics {
        +static string VersionName
        +static string VersionCode
    }

    HttpRequest ..> HttpSenderExtension : uses SendAsync()
    HttpRequest ..> HttpContentExtension : uses WithJsonContent()
    HttpRequest ..> HttpResponseExtension : uses AsStringAsync(), AsJsonAsync()

    HttpSenderExtension ..> NetworkService : uses GetClient(), GetRetryPolicy()
    HttpSenderExtension ..> LogWrapper : logging
    HttpSenderExtension ..> Basics : request metadata

    HttpContentExtension ..> HttpHeaderHandler : WithHeader()
    HttpHeaderHandler ..> HttpRequestMessage : modifies
    HttpCookieExtension ..> HttpRequestMessage : modifies
    HttpBasicExtension ..> HttpRequestMessage : modifies

    HttpResponseExtension ..> HttpResponseMessage : extends
Loading

File-Level Changes

Change Details Files
Introduce new fluent HTTP client helper APIs and deprecate the previous HttpRequestBuilder/HttpResponseHandler abstractions.
  • Add HttpRequest factory with helpers for GET/HEAD/POST/PUT/DELETE and high-level GetStringAsync/GetJsonAsync/PostJsonAsync methods.
  • Add extension-based helpers on HttpRequestMessage for headers, cookies, content, HTTP version, and sending with retry, logging, and default metadata.
  • Add HttpResponseMessage extensions for reading content (string/stream/bytes/JSON), header access helpers, and success validation methods including EnsureSuccessStatusCodeWithContentAsync.
  • Remove legacy HttpRequestBuilder and HttpResponseHandler types in favor of the new extension-based model.
PCL.Core/IO/Net/Http/HttpRequest.cs
PCL.Core/IO/Net/Http/HttpSenderExtension.cs
PCL.Core/IO/Net/Http/HttpContentExtension.cs
PCL.Core/IO/Net/Http/HttpHeaderExtension.cs
PCL.Core/IO/Net/Http/HttpCookieExtension.cs
PCL.Core/IO/Net/Http/HttpBasicExtension.cs
PCL.Core/IO/Net/Http/HttpResponseExtension.cs
PCL.Core/IO/Net/Http/Client/HttpRequestBuilder.cs
PCL.Core/IO/Net/Http/Client/HttpResponseHandler.cs
Migrate authentication, identity, and Yggdrasil-related HTTP calls to the new HttpRequest API with JSON helpers.
  • Refactor YggdrasilLegacyClient methods to build POST requests with HttpRequest.CreatePost, apply headers, send with HttpClient from options, and deserialize responses via AsJsonAsync.
  • Refactor OAuth client methods (authorization, device code, refresh) to construct requests with HttpRequest + WithContent/WithHeaders and parse responses using AsJsonAsync.
  • Update OpenIdOptions and YggdrasilOptions initialization and key retrieval to use HttpRequest.Create + WithHeaders + AsJsonAsync instead of manual HttpRequestMessage and JsonSerializer usage.
  • Adjust ApiLocation.TryRequestAsync to use HttpRequest.CreateHead and new header access helpers (TryGetHeader) for X-Authlib-Injector-Api-Location handling.
PCL.Core/Minecraft/IdentityModel/Yggdrasil/Client.cs
PCL.Core/Minecraft/IdentityModel/OAuth/Client.cs
PCL.Core/Minecraft/IdentityModel/Extensions/OpenId/OpenIdOptions.cs
PCL.Core/Minecraft/IdentityModel/Extensions/YggdrasilConnect/YggdrasilOptions.cs
PCL.Core/Minecraft/Yggdrasil/ApiLocation.cs
Update launcher VB modules and pages to use the new HTTP helpers, add explicit success checks, and simplify response handling.
  • Replace HttpRequestBuilder usage with HttpRequest.* in ModLaunch.vb for Microsoft/Xbox/Minecraft auth flows, switching to WithFormContent/WithJsonContent, SendAsync, EnsureSuccessStatusCode, and AsString.
  • Update ModDownload.vb and ModDownloadLib.vb to fetch OptiFine and other metadata using HttpRequest.Create, SendAsync, EnsureSuccessStatusCode, and AsString, including minor whitespace and variable fixes.
  • Adjust various update-related models (UpdatesMinioModel, UpdatesMirrorChyanModel, UpdatesHomepageMarket) to call HttpRequest.Create().SendAsync() and read content with AsString/AsJsonAsync, adding EnsureSuccessStatusCode where appropriate.
  • Refactor PageLoginAuth, MyImage, PageSetupAbout, and NewsViewModel to use HttpRequest.Create, SendAsync, and AsString/AsJsonAsync, replacing IsSuccess with IsSuccessStatusCode and disabling default headers where required.
Plain Craft Launcher 2/Modules/Minecraft/ModLaunch.vb
Plain Craft Launcher 2/Modules/Minecraft/ModDownload.vb
Plain Craft Launcher 2/Modules/PageDownload/ModDownloadLib.vb
Plain Craft Launcher 2/Modules/Updates/UpdatesMinioModel.vb
Plain Craft Launcher 2/Modules/Updates/UpdatesMirrorChyanModel.vb
Plain Craft Launcher 2/Pages/PageLaunch/PageLoginAuth.xaml.vb
Plain Craft Launcher 2/Controls/MyImage.vb
Plain Craft Launcher 2/Pages/PageSetup/PageSetupAbout.xaml.vb
Plain Craft Launcher 2/Pages/PageSetup/PageHomePageMarket.xaml.vb
PCL.Core/ViewModel/Homepage/NewsViewModel.cs
Rework NetworkService lifecycle integration and HTTP retry policy, and update code that depends on its behavior.
  • Replace GeneralService inheritance with attribute-based lifecycle ([LifecycleService] and [LifecycleScope]) and static start/stop methods marked with [LifecycleStart]/[LifecycleStop].
  • Configure the default HttpClient in NetworkService with tuned SocketsHttpHandler settings (proxy, decompression, DoH connection callback, MaxAutomaticRedirections reduced to 20).
  • Redefine retry backoff logic to use an exponential strategy with configurable base/max delay constants, and change GetRetryPolicy to use a synchronous onRetry logger with richer messages.
  • Update HttpSenderExtension.SendAsync to rely on NetworkService.GetRetryPolicy, clone requests per attempt, and log per-request lifecycle and retry attempts with a correlation id.
PCL.Core/IO/Net/NetworkService.cs
PCL.Core/IO/Net/Http/HttpSenderExtension.cs
Adjust internal HTTP server types’ namespaces and corresponding references.
  • Move HttpRoute, HttpRouteResponse, and HttpServer from the PCL.Core.IO.Net.Http.Server namespace into PCL.Core.IO.Net.Http.
  • Update using/import statements in tests and VB modules (e.g., ModWebServer, WebServerTest) to reference the new namespace.
PCL.Core/IO/Net/Http/HttpRoute.cs
PCL.Core/IO/Net/Http/HttpRouteResponse.cs
PCL.Core/IO/Net/Http/HttpServer.cs
PCL.Core.Test/Network/WebServerTest.cs
Plain Craft Launcher 2/Modules/ModWebServer.vb
Align external integration modules (Natayark, EasyTier, Lobby, Telemetry) with the new HTTP abstractions and semantics.
  • Refactor NatayarkProfileManager to use HttpRequest.Create/CreatePost, WithContent/WithBearerToken, SendAsync, EnsureSuccessStatusCode, and AsStringAsync, tightening null checks on responses.
  • Replace EasyTier public node retrieval’s custom Polly logic with simple HttpRequest.Create().SendAsync().EnsureSuccessStatusCode() and JSON parsing via AsJsonAsync.
  • Update LobbyController telemetry POST to use HttpRequest.CreatePost + WithContent + WithBearerToken + SendAsync, and adjust success checks to rely on new response helper properties.
  • Modify TelemetryService to send environment data via HttpRequest.CreatePost + WithHeader("Authorization", key) + WithJsonContent + SendAsync, but still use the IsSuccess helper property from HttpResponseExtension.
PCL.Core/Link/Natayark/NatayarkProfileManager.cs
PCL.Core/Link/Scaffolding/EasyTier/EasyTierEntity.cs
PCL.Core/Link/Lobby/LobbyController.cs
PCL.Core/App/Essentials/TelemetryService.cs
Apply smaller correctness and style fixes uncovered during the refactor.
  • Fix DlNeoForgeListMain to consistently use the local loader parameter instead of Loader, and propagate IsForceRestarting from the correct instance.
  • Normalize several imports from PCL.Core.IO.Net.Http.Client to PCL.Core.IO.Net.Http.Client.Request in VB and C# files to use the new helper namespace.
  • Tidy a few minor formatting issues (whitespace, braces, request initialization style) and ensure EnsureSuccessStatusCode is used before processing responses in new call sites.
  • Keep the DohConnection unit test logic consistent while minorly adjusting HttpRequestMessage initialization style.
Plain Craft Launcher 2/Modules/Minecraft/ModDownload.vb
Plain Craft Launcher 2/Pages/PageLaunch/PageLoginAuth.xaml.vb
Plain Craft Launcher 2/Modules/Minecraft/ModLaunch.vb
PCL.Core.Test/Network/DohConnection.cs
Plain Craft Launcher 2/Modules/Updates/UpdatesMinioModel.vb
Plain Craft Launcher 2/Modules/Updates/UpdatesMirrorChyanModel.vb
Plain Craft Launcher 2/Controls/MyImage.vb
Plain Craft Launcher 2/Modules/ModWebServer.vb
PCL.Core/Link/Lobby/LobbyController.cs
PCL.Core/App/Essentials/TelemetryService.cs

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.

你好——我这里发现了 3 个问题,并给出了一些整体性反馈:

  • 新的 HttpSenderExtension.SendAsync 使用了 NetworkService.GetRetryPolicy(其返回的是非泛型的 AsyncPolicy),却把它当成 AsyncPolicy<HttpResponseMessage> 来用,在委托里直接返回了 HttpResponseMessage;这在 Polly 中是无法编译通过的。要么让 GetRetryPolicy 变成泛型(例如返回 AsyncPolicy<HttpResponseMessage>),要么在 SendAsync 中将结果包装进一个非泛型的重试策略里。
  • 一些已经接受 CancellationToken 的异步辅助方法(例如 YggdrasilLegacyClient.RefreshAsync 中的部分内容、一些 AsJsonAsync/AsStringAsync 的调用,以及 OpenIdOptions.InitializeAsync),在调用新的响应扩展方法时没有始终把该 token 继续往下传。建议在所有能拿到 token 的地方都把它沿用下去,以保持取消行为的一致性。
给 AI 代理的提示
Please address the comments from this code review:

## Overall Comments
- The new `HttpSenderExtension.SendAsync` uses `NetworkService.GetRetryPolicy` (which returns a non-generic `AsyncPolicy`) as if it were `AsyncPolicy<HttpResponseMessage>` by returning an `HttpResponseMessage` from the delegate; this won’t compile with Polly and either `GetRetryPolicy` should become generic (e.g. `AsyncPolicy<HttpResponseMessage>`) or `SendAsync` should wrap the result in a non-generic retry policy.
- Several asynchronous helpers that already accept a `CancellationToken` (e.g. in `YggdrasilLegacyClient.RefreshAsync`, some `AsJsonAsync`/`AsStringAsync` calls, and `OpenIdOptions.InitializeAsync`) don’t consistently pass the token down to the new response extension methods; consider threading the token through everywhere it’s available to keep cancellation behavior consistent.

## Individual Comments

### Comment 1
<location path="PCL.Core/IO/Net/Http/HttpResponseExtension.cs" line_range="14" />
<code_context>
+
+public static class HttpResponseExtension
+{
+    extension(HttpResponseMessage responseMessage)
+    {
+        public bool IsSuccess => responseMessage.IsSuccessStatusCode;
</code_context>
<issue_to_address>
**issue (bug_risk):** The `extension(HttpResponseMessage ...)` construct is not valid C# and will prevent these helpers from compiling or being used as extension methods.

Extension methods in C# must be `public static` methods with a `this` parameter inside a non-generic `static` class, e.g. `public static string AsString(this HttpResponseMessage responseMessage)`. The `extension(HttpResponseMessage responseMessage)` block syntax is not valid C# and will not compile, so usages like `response.AsString()` / `request.WithJsonContent(...)` will not bind. Please convert these helpers to standard extension-method declarations.
</issue_to_address>

### Comment 2
<location path="PCL.Core/Minecraft/IdentityModel/Yggdrasil/Client.cs" line_range="138" />
<code_context>
+        using var response = await HttpRequest
+            .CreatePost(address)
+            .WithHeaders(options.Headers ?? [])
+            .WithJsonContent(signoutData)
+            .SendAsync(options.GetClient.Invoke(), cancellationToken: token)
+            .ConfigureAwait(false);
</code_context>
<issue_to_address>
**issue (bug_risk):** Passing an already-serialized JSON string to `WithJsonContent` will double-encode the payload.

Here `signoutData` is already serialized via `.ToJsonString()`, so passing it to `WithJsonContent` causes it to be serialized a second time. That yields a JSON string literal (quoted JSON) instead of the expected JSON object, which will likely break the Yggdrasil signout API.

Either pass the anonymous object / `JsonObject` directly to `WithJsonContent`, or switch to `WithContent(signoutData, "application/json")` to use the pre-serialized string as-is.
</issue_to_address>

### Comment 3
<location path="PCL.Core/Minecraft/IdentityModel/Extensions/OpenId/OpenIdOptions.cs" line_range="62-65" />
<code_context>
-            .Create("https://pcl2ce.pysio.online/post", HttpMethod.Post)
-            .WithAuthentication(telemetryKey).WithJsonContent(telemetry)
-            .SendAsync().ConfigureAwait(false);
+        using var response = await HttpRequest
+            .CreatePost("https://pcl2ce.pysio.online/post")
+            .WithHeader("Authorization", telemetryKey)
</code_context>
<issue_to_address>
**suggestion (bug_risk):** The cancellation token is no longer honored when sending the discovery request.

Previously, `InitializeAsync` called `SendAsync(request, token)`, so the caller’s `CancellationToken` could cancel the HTTP request itself. The new code calls `SendAsync(GetClient.Invoke())` without the token, meaning cancellation now only affects deserialization, not long/stuck network calls.

Please pass the token through to `SendAsync` (e.g. `SendAsync(GetClient.Invoke(), cancellationToken: token)`) to preserve the original cancellation behavior.
</issue_to_address>

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

Hey - I've found 3 issues, and left some high level feedback:

  • The new HttpSenderExtension.SendAsync uses NetworkService.GetRetryPolicy (which returns a non-generic AsyncPolicy) as if it were AsyncPolicy<HttpResponseMessage> by returning an HttpResponseMessage from the delegate; this won’t compile with Polly and either GetRetryPolicy should become generic (e.g. AsyncPolicy<HttpResponseMessage>) or SendAsync should wrap the result in a non-generic retry policy.
  • Several asynchronous helpers that already accept a CancellationToken (e.g. in YggdrasilLegacyClient.RefreshAsync, some AsJsonAsync/AsStringAsync calls, and OpenIdOptions.InitializeAsync) don’t consistently pass the token down to the new response extension methods; consider threading the token through everywhere it’s available to keep cancellation behavior consistent.
Prompt for AI Agents
Please address the comments from this code review:

## Overall Comments
- The new `HttpSenderExtension.SendAsync` uses `NetworkService.GetRetryPolicy` (which returns a non-generic `AsyncPolicy`) as if it were `AsyncPolicy<HttpResponseMessage>` by returning an `HttpResponseMessage` from the delegate; this won’t compile with Polly and either `GetRetryPolicy` should become generic (e.g. `AsyncPolicy<HttpResponseMessage>`) or `SendAsync` should wrap the result in a non-generic retry policy.
- Several asynchronous helpers that already accept a `CancellationToken` (e.g. in `YggdrasilLegacyClient.RefreshAsync`, some `AsJsonAsync`/`AsStringAsync` calls, and `OpenIdOptions.InitializeAsync`) don’t consistently pass the token down to the new response extension methods; consider threading the token through everywhere it’s available to keep cancellation behavior consistent.

## Individual Comments

### Comment 1
<location path="PCL.Core/IO/Net/Http/HttpResponseExtension.cs" line_range="14" />
<code_context>
+
+public static class HttpResponseExtension
+{
+    extension(HttpResponseMessage responseMessage)
+    {
+        public bool IsSuccess => responseMessage.IsSuccessStatusCode;
</code_context>
<issue_to_address>
**issue (bug_risk):** The `extension(HttpResponseMessage ...)` construct is not valid C# and will prevent these helpers from compiling or being used as extension methods.

Extension methods in C# must be `public static` methods with a `this` parameter inside a non-generic `static` class, e.g. `public static string AsString(this HttpResponseMessage responseMessage)`. The `extension(HttpResponseMessage responseMessage)` block syntax is not valid C# and will not compile, so usages like `response.AsString()` / `request.WithJsonContent(...)` will not bind. Please convert these helpers to standard extension-method declarations.
</issue_to_address>

### Comment 2
<location path="PCL.Core/Minecraft/IdentityModel/Yggdrasil/Client.cs" line_range="138" />
<code_context>
+        using var response = await HttpRequest
+            .CreatePost(address)
+            .WithHeaders(options.Headers ?? [])
+            .WithJsonContent(signoutData)
+            .SendAsync(options.GetClient.Invoke(), cancellationToken: token)
+            .ConfigureAwait(false);
</code_context>
<issue_to_address>
**issue (bug_risk):** Passing an already-serialized JSON string to `WithJsonContent` will double-encode the payload.

Here `signoutData` is already serialized via `.ToJsonString()`, so passing it to `WithJsonContent` causes it to be serialized a second time. That yields a JSON string literal (quoted JSON) instead of the expected JSON object, which will likely break the Yggdrasil signout API.

Either pass the anonymous object / `JsonObject` directly to `WithJsonContent`, or switch to `WithContent(signoutData, "application/json")` to use the pre-serialized string as-is.
</issue_to_address>

### Comment 3
<location path="PCL.Core/Minecraft/IdentityModel/Extensions/OpenId/OpenIdOptions.cs" line_range="62-65" />
<code_context>
-            .Create("https://pcl2ce.pysio.online/post", HttpMethod.Post)
-            .WithAuthentication(telemetryKey).WithJsonContent(telemetry)
-            .SendAsync().ConfigureAwait(false);
+        using var response = await HttpRequest
+            .CreatePost("https://pcl2ce.pysio.online/post")
+            .WithHeader("Authorization", telemetryKey)
</code_context>
<issue_to_address>
**suggestion (bug_risk):** The cancellation token is no longer honored when sending the discovery request.

Previously, `InitializeAsync` called `SendAsync(request, token)`, so the caller’s `CancellationToken` could cancel the HTTP request itself. The new code calls `SendAsync(GetClient.Invoke())` without the token, meaning cancellation now only affects deserialization, not long/stuck network calls.

Please pass the token through to `SendAsync` (e.g. `SendAsync(GetClient.Invoke(), cancellationToken: token)`) to preserve the original cancellation behavior.
</issue_to_address>

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.

Copy link
Copy Markdown
Member

@lhx077 lhx077 left a comment

Choose a reason for hiding this comment

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

LGTM

@pcl-ce-automation pcl-ce-automation bot added 🕑 等待合并 已处理完毕,正在等待代码合并入主分支 and removed 🛠️ 等待审查 Pull Request 已完善,等待维护者或负责人进行代码审查 labels Mar 17, 2026
@tangge233 tangge233 merged commit 5b7f753 into dev Mar 18, 2026
3 checks passed
@pcl-ce-automation pcl-ce-automation bot added 👌 完成 相关问题已修复或功能已实现,计划在下次版本更新时正式上线 and removed 🕑 等待合并 已处理完毕,正在等待代码合并入主分支 labels Mar 18, 2026
@tangge233 tangge233 deleted the refactor/request branch March 18, 2026 02:59
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

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

Projects

None yet

Development

Successfully merging this pull request may close these issues.

4 participants