From 9859df5831fa7561ef0f70b112a398544d26a645 Mon Sep 17 00:00:00 2001 From: JusterZhu Date: Sat, 20 Jun 2026 00:42:30 +0800 Subject: [PATCH 1/3] feat(core): unified HTTP auth provider + custom headers + CVP fix - HttpClientProvider: add DefaultAuthProvider, ExtraHeaders, ApplyAuthAsync() - VersionService: delegate _globalAuthProvider to HttpClientProvider; rename fields (_auth->_authProvider, _maxRetries->_maxRetryAttempts, IsRetryable->ShouldRetry) + constructor parameter rename - HttpUpdateReporter: use HttpClientProvider.Shared instead of new HttpClient(); apply global auth provider before sending - AbstractBootstrap: add HttpAuth(IHttpAuthProvider) instance overload - UpdateConfiguration: add CustomHeaders dictionary for tenant/extra headers - ConfigurationMapper: map CustomHeaders field - SetConfig: auto-sync CustomHeaders to HttpClientProvider.ExtraHeaders - ClientTest: use SetConfig with CustomHeaders instead of manual header; fix manifest productId to match test data Co-Authored-By: Claude --- .../Bootstrap/GeneralUpdateBootstrap.cs | 9 +++ .../Configuration/AbstractBootstrap.cs | 16 +++++ .../Configuration/ConfigurationMapper.cs | 1 + .../Configuration/UpdateConfiguration.cs | 13 ++++ .../Download/Reporting/IUpdateReporter.cs | 12 +++- .../Network/HttpClientProvider.cs | 61 +++++++++++++++++-- .../Network/VersionService.cs | 42 +++++++------ tests/ClientTest/Program.cs | 24 ++++++-- tests/ClientTest/generalupdate.manifest.json | 2 +- 9 files changed, 150 insertions(+), 30 deletions(-) diff --git a/src/c#/GeneralUpdate.Core/Bootstrap/GeneralUpdateBootstrap.cs b/src/c#/GeneralUpdate.Core/Bootstrap/GeneralUpdateBootstrap.cs index aa74f55c..e479457c 100644 --- a/src/c#/GeneralUpdate.Core/Bootstrap/GeneralUpdateBootstrap.cs +++ b/src/c#/GeneralUpdate.Core/Bootstrap/GeneralUpdateBootstrap.cs @@ -14,6 +14,7 @@ using GeneralUpdate.Core.Ipc; using GeneralUpdate.Core.Differential; using GeneralUpdate.Core.Pipeline; +using GeneralUpdate.Core.Network; using GeneralUpdate.Differential.Differ; namespace GeneralUpdate.Core; @@ -198,6 +199,14 @@ public GeneralUpdateBootstrap SetConfig(UpdateRequest configInfo) configInfo.Validate(); _configInfo = ConfigurationMapper.MapToUpdateContext(configInfo); + // Sync custom headers to the global HTTP client so they are applied + // to every request (Verification + Report) without manual extra setup. + if (_configInfo.CustomHeaders is { Count: > 0 }) + { + foreach (var kv in _configInfo.CustomHeaders) + HttpClientProvider.ExtraHeaders[kv.Key] = kv.Value; + } + var appType = GetOption(Option.AppType); if (appType != AppType.Upgrade) { diff --git a/src/c#/GeneralUpdate.Core/Configuration/AbstractBootstrap.cs b/src/c#/GeneralUpdate.Core/Configuration/AbstractBootstrap.cs index c70de9b0..92c39ac1 100644 --- a/src/c#/GeneralUpdate.Core/Configuration/AbstractBootstrap.cs +++ b/src/c#/GeneralUpdate.Core/Configuration/AbstractBootstrap.cs @@ -260,6 +260,22 @@ protected T GetOption(Option? option) return (TBootstrap)this; } + /// + /// Registers a pre-constructed authentication provider instance. + /// Use this overload when the provider requires constructor parameters + /// (e.g. Basic Auth with username/password, or a custom provider with + /// a tenant header). For parameterless providers, prefer . + /// + /// The authentication provider instance. + /// The current instance for chaining. + /// Thrown when is null. + public TBootstrap HttpAuth(Security.IHttpAuthProvider provider) + { + _instances[typeof(Security.IHttpAuthProvider)] = + provider ?? throw new ArgumentNullException(nameof(provider)); + return (TBootstrap)this; + } + /// /// Registers an HTTP authentication provider for attaching authentication credentials /// to download requests. diff --git a/src/c#/GeneralUpdate.Core/Configuration/ConfigurationMapper.cs b/src/c#/GeneralUpdate.Core/Configuration/ConfigurationMapper.cs index ae9e110e..e5dffa23 100644 --- a/src/c#/GeneralUpdate.Core/Configuration/ConfigurationMapper.cs +++ b/src/c#/GeneralUpdate.Core/Configuration/ConfigurationMapper.cs @@ -102,6 +102,7 @@ public static UpdateContext MapToUpdateContext(UpdateRequest source, UpdateConte target.UpdateUrl = source.UpdateUrl; target.UpgradeClientVersion = source.UpgradeClientVersion; target.ProductId = source.ProductId; + target.CustomHeaders = source.CustomHeaders; return target; } diff --git a/src/c#/GeneralUpdate.Core/Configuration/UpdateConfiguration.cs b/src/c#/GeneralUpdate.Core/Configuration/UpdateConfiguration.cs index ec6eb15e..7cb42083 100644 --- a/src/c#/GeneralUpdate.Core/Configuration/UpdateConfiguration.cs +++ b/src/c#/GeneralUpdate.Core/Configuration/UpdateConfiguration.cs @@ -231,5 +231,18 @@ public abstract class UpdateConfiguration /// The unique product identifier for the current application. /// public string ProductId { get; set; } + + /// + /// Custom HTTP headers to include in every request made by the update framework + /// (both version validation and status reporting). + /// Headers are applied after the authentication provider, so they can supplement + /// but not override authentication headers. + /// + /// + /// Example: add ["X-Tenant-Id"] = "default-tenant" for multi-tenant servers. + /// Headers set here are synced to + /// at configuration time. + /// + public Dictionary CustomHeaders { get; set; } } } diff --git a/src/c#/GeneralUpdate.Core/Download/Reporting/IUpdateReporter.cs b/src/c#/GeneralUpdate.Core/Download/Reporting/IUpdateReporter.cs index 6a177128..8290c468 100644 --- a/src/c#/GeneralUpdate.Core/Download/Reporting/IUpdateReporter.cs +++ b/src/c#/GeneralUpdate.Core/Download/Reporting/IUpdateReporter.cs @@ -4,6 +4,7 @@ using System.Text.Json; using System.Threading; using System.Threading.Tasks; +using GeneralUpdate.Core.Network; namespace GeneralUpdate.Core.Download.Reporting; @@ -111,11 +112,13 @@ public HttpClient Client /// /// Parameterless constructor required by the extension resolution mechanism. - /// Uses a default HttpClient and empty ReportUrl (no-op until configured). + /// Uses the shared instance and empty ReportUrl + /// (no-op until configured). The shared HttpClient honours the global SSL policy set + /// via . /// public HttpUpdateReporter() { - _client = new HttpClient(); + _client = HttpClientProvider.Shared; _reportUrl = string.Empty; } @@ -142,6 +145,11 @@ public async Task ReportAsync(UpdateReport report, CancellationToken token = def using var request = new HttpRequestMessage(HttpMethod.Post, _reportUrl); request.Content = new StringContent(json, Encoding.UTF8, "application/json"); + // Apply the global authentication provider (if one was set via + // VersionService.SetDefaultAuthProvider or HttpClientProvider.DefaultAuthProvider) + // so that report requests carry the same credentials as version validation requests. + await HttpClientProvider.ApplyAuthAsync(request, token).ConfigureAwait(false); + await _client.SendAsync(request, token).ConfigureAwait(false); } catch (Exception ex) diff --git a/src/c#/GeneralUpdate.Core/Network/HttpClientProvider.cs b/src/c#/GeneralUpdate.Core/Network/HttpClientProvider.cs index 824e3371..d623992a 100644 --- a/src/c#/GeneralUpdate.Core/Network/HttpClientProvider.cs +++ b/src/c#/GeneralUpdate.Core/Network/HttpClientProvider.cs @@ -1,15 +1,19 @@ using System; +using System.Collections.Concurrent; +using System.Linq; using System.Net.Http; using System.Net.Security; using System.Security.Cryptography.X509Certificates; +using System.Threading; +using System.Threading.Tasks; using GeneralUpdate.Core.Security; namespace GeneralUpdate.Core.Network; /// -/// Provides a shared static instance with configurable SSL validation. -/// Reusing a single HttpClient prevents socket exhaustion. -/// Do NOT dispose clients obtained from here. +/// Provides a shared static instance with configurable SSL validation +/// and a global HTTP authentication provider. Reusing a single HttpClient prevents socket +/// exhaustion. Do NOT dispose clients obtained from here. /// /// /// @@ -18,13 +22,15 @@ namespace GeneralUpdate.Core.Network; /// (rejects any certificate with SSL errors). /// /// -/// This class mirrors the SSL validation pattern used by , -/// ensuring that both API calls and file downloads respect the same global SSL policy. +/// The global set via is +/// applied by so that components like +/// participate in the same authentication scheme as . /// /// public static class HttpClientProvider { private static ISslValidationPolicy _sslPolicy = new StrictSslValidationPolicy(); + private static IHttpAuthProvider? _defaultAuthProvider; private static readonly HttpClient _shared; static HttpClientProvider() @@ -67,4 +73,49 @@ public static void SetSslValidationPolicy(ISslValidationPolicy policy) /// Gets the shared instance. Do NOT dispose. /// public static HttpClient Shared => _shared; + + /// + /// Gets or sets the global default HTTP authentication provider. + /// When set, every HTTP call from , + /// , and other components using + /// will carry the configured credentials. + /// + public static IHttpAuthProvider? DefaultAuthProvider + { + get => _defaultAuthProvider; + set => _defaultAuthProvider = value; + } + + /// + /// Extra HTTP headers applied to every outgoing request by . + /// Use this for headers that are not part of the authentication scheme itself but + /// are required by the target server — for example X-Tenant-Id for tenant scoping. + /// + /// + /// The key is the header name, the value is the header value. + /// Existing headers of the same name on the request are overwritten. + /// This dictionary is thread-safe. + /// + public static ConcurrentDictionary ExtraHeaders { get; } + = new ConcurrentDictionary(StringComparer.OrdinalIgnoreCase); + + /// + /// Applies the global default authentication provider and extra headers + /// to the specified request. When no global provider is configured, + /// the auth step is a no-op. + /// + /// The HTTP request message to authenticate. + /// A cancellation token for the authentication operation. + /// A task representing the asynchronous operation. + public static async Task ApplyAuthAsync(HttpRequestMessage request, CancellationToken token = default) + { + if (_defaultAuthProvider != null) + await _defaultAuthProvider.ApplyAuthAsync(request, token).ConfigureAwait(false); + + foreach (var kv in ExtraHeaders) + { + request.Headers.Remove(kv.Key); + request.Headers.Add(kv.Key, kv.Value); + } + } } diff --git a/src/c#/GeneralUpdate.Core/Network/VersionService.cs b/src/c#/GeneralUpdate.Core/Network/VersionService.cs index e2965e20..7c7835a0 100644 --- a/src/c#/GeneralUpdate.Core/Network/VersionService.cs +++ b/src/c#/GeneralUpdate.Core/Network/VersionService.cs @@ -66,11 +66,10 @@ public class VersionService { private static readonly HttpClient _sharedClient; private static volatile ISslValidationPolicy _globalSslPolicy = new StrictSslValidationPolicy(); - private static IHttpAuthProvider? _globalAuthProvider; - private readonly IHttpAuthProvider _auth; + private readonly IHttpAuthProvider _authProvider; private readonly TimeSpan _timeout; - private readonly int _maxRetries; + private readonly int _maxRetryAttempts; /// /// Static constructor: initializes the static members of . @@ -111,18 +110,25 @@ public static void SetSslValidationPolicy(ISslValidationPolicy policy) /// Sets the global default HTTP authentication provider. /// /// + /// + /// Delegates to so that + /// and other components using + /// share the same global authentication. + /// + /// /// When a global authentication provider is set, all requests made via the static APIs /// ( /// and ) /// will preferentially use this provider, overriding the authentication instance /// created by . + /// /// /// Passing null clears the global authentication provider, reverting to the factory method. /// /// /// The global authentication provider instance, or null to clear the global configuration. public static void SetDefaultAuthProvider(IHttpAuthProvider? provider) - => _globalAuthProvider = provider; + => HttpClientProvider.DefaultAuthProvider = provider; private static bool SharedCertValidation(HttpRequestMessage m, X509Certificate2? c, X509Chain? ch, SslPolicyErrors e) @@ -134,16 +140,16 @@ private static bool SharedCertValidation(HttpRequestMessage m, X509Certificate2? /// /// Instance methods ( and ) use /// this instance's authentication provider and timeout settings. - /// When is null, (no authentication) is used by default. + /// When is null, (no authentication) is used by default. /// - /// The HTTP authentication provider. If null, is used. - /// The request timeout. If null, defaults to 30 seconds. - /// The maximum number of retry attempts. Defaults to 3. - public VersionService(IHttpAuthProvider? auth = null, TimeSpan? timeout = null, int maxRetries = 3) + /// The HTTP authentication provider. If null, is used. + /// The request timeout. If null, defaults to 30 seconds. + /// The maximum number of retry attempts. Defaults to 3. + public VersionService(IHttpAuthProvider? authProvider = null, TimeSpan? requestTimeout = null, int maxRetryAttempts = 3) { - _auth = auth ?? new NoOpAuthProvider(); - _timeout = timeout ?? TimeSpan.FromSeconds(30); - _maxRetries = maxRetries; + _authProvider = authProvider ?? new NoOpAuthProvider(); + _timeout = requestTimeout ?? TimeSpan.FromSeconds(30); + _maxRetryAttempts = maxRetryAttempts; } /// @@ -185,7 +191,7 @@ public static Task Validate(string url, string version, AppType appType, string appKey, PlatformType platform, string productId, string scheme = null, string token = null, Security.AuthScheme authScheme = Security.AuthScheme.Hmac, string basicUsername = null, string basicPassword = null, CancellationToken ct = default) { - var auth = _globalAuthProvider ?? HttpAuthProviderFactory.Create(scheme, token, appKey, authScheme, basicUsername, basicPassword); + var auth = HttpClientProvider.DefaultAuthProvider ?? HttpAuthProviderFactory.Create(scheme, token, appKey, authScheme, basicUsername, basicPassword); return new VersionService(auth).ValidateAsync(url, version, (int)appType, appKey, (int)platform, productId, ct); } @@ -288,9 +294,9 @@ private async Task PostAsync(string url, Dictionary p, for (int attempt = 0; ; attempt++) { try { return await SendAsync(url, p, ti, t).ConfigureAwait(false); } - catch (Exception ex) when (attempt < _maxRetries - 1 && IsRetryable(ex)) + catch (Exception ex) when (attempt < _maxRetryAttempts - 1 && ShouldRetry(ex)) { - GeneralTracer.Warn($"HTTP attempt {attempt + 1}/{_maxRetries} failed, retrying. {ex.Message}"); + GeneralTracer.Warn($"HTTP attempt {attempt + 1}/{_maxRetryAttempts} failed, retrying. {ex.Message}"); await Task.Delay(TimeSpan.FromMilliseconds(Math.Pow(2, attempt) * 1000), t).ConfigureAwait(false); } } @@ -343,7 +349,7 @@ private async Task SendAsync(string url, Dictionary p, req.Headers.Accept.ParseAdd("application/json"); var json = JsonSerializer.Serialize(p, HttpParameterJsonContext.Default.DictionaryStringObject); req.Content = new StringContent(json, Encoding.UTF8, "application/json"); - await _auth.ApplyAuthAsync(req, t).ConfigureAwait(false); + await _authProvider.ApplyAuthAsync(req, t).ConfigureAwait(false); using var cts = CancellationTokenSource.CreateLinkedTokenSource(t); cts.CancelAfter(_timeout); @@ -354,7 +360,7 @@ private async Task SendAsync(string url, Dictionary p, } /// - /// Determines whether an exception is retryable. + /// Determines whether an exception should be retried. /// /// The exception to evaluate. /// true if the exception is retryable; otherwise false. @@ -375,7 +381,7 @@ private async Task SendAsync(string url, Dictionary p, /// /// /// - private static bool IsRetryable(Exception ex) + private static bool ShouldRetry(Exception ex) { if (ex is OperationCanceledException) return false; if (ex is TaskCanceledException or TimeoutException or System.IO.IOException) return true; diff --git a/tests/ClientTest/Program.cs b/tests/ClientTest/Program.cs index 7590d8c9..f0772482 100644 --- a/tests/ClientTest/Program.cs +++ b/tests/ClientTest/Program.cs @@ -1,9 +1,12 @@ +using System.Collections.Generic; + using GeneralUpdate.Core; using GeneralUpdate.Core.Configuration; using GeneralUpdate.Core.Download; using GeneralUpdate.Core.Download.Reporting; using GeneralUpdate.Core.Event; using GeneralUpdate.Core.Hooks; +using GeneralUpdate.Core.Security; try { @@ -42,12 +45,13 @@ static async Task RunUpdateTestAsync() var updateUrl = "http://localhost:7391/Upgrade/Verification"; var reportUrl = "http://localhost:7391/Upgrade/Report"; - var appSecretKey = - Environment.GetEnvironmentVariable("APP_SECRET_KEY") - ?? "dfeb5833-975e-4afb-88f1-6278ee9aeff6"; + var appSecretKey = "dfeb5833-975e-4afb-88f1-6278ee9aeff6"; Console.WriteLine($"UpdateUrl: {updateUrl}"); + Console.WriteLine($"ReportUrl: {reportUrl}"); + Console.WriteLine($"TenantId: default-tenant"); Console.WriteLine(); + Console.WriteLine("NOTE: Configure the GeneralSpacestation server with"); Console.WriteLine(" the desired test data before running."); Console.WriteLine(" - Chain data: TbPackets (IsCrossVersion=false)"); @@ -67,7 +71,19 @@ static async Task RunUpdateTestAsync() // - Both: apply upgrade packages → IPC → launch Upgrade process → exit // - None: no-op await new GeneralUpdateBootstrap() - .SetSource(updateUrl, appSecretKey, reportUrl) + .SetConfig(new UpdateRequest + { + UpdateUrl = updateUrl, + AppSecretKey = appSecretKey, + ReportUrl = reportUrl, + AuthScheme = AuthScheme.Basic, + BasicUsername = "test", + BasicPassword = "test", + CustomHeaders = new Dictionary + { + ["X-Tenant-Id"] = "default-tenant" + } + }) .SetOption(Option.AppType, AppType.Client) .Hooks() .AddListenerMultiDownloadStatistics(OnDownloadStatistics) diff --git a/tests/ClientTest/generalupdate.manifest.json b/tests/ClientTest/generalupdate.manifest.json index 79be2a0a..f31e76ba 100644 --- a/tests/ClientTest/generalupdate.manifest.json +++ b/tests/ClientTest/generalupdate.manifest.json @@ -4,6 +4,6 @@ "appType": "Client", "updateAppName": "UpgradeTest.exe", "upgradeClientVersion": "2.0.0.0", - "productId": "2d974e2a-31e6-4887-9bb1-b4689e98c77a", + "productId": "8d74e7eb-160c-4a4b-a5dd-c71a7bb08eeb", "updatePath": "update/" } From 21e4f5df1769830168acbc278b785f4fe634b87d Mon Sep 17 00:00:00 2001 From: JusterZhu Date: Sat, 20 Jun 2026 01:01:07 +0800 Subject: [PATCH 2/3] fix: address Copilot review comments - Remove unused using System.Linq from HttpClientProvider - Handle content headers (Content-Type etc.) in ApplyAuthAsync to avoid InvalidOperationException from HttpRequestHeaders - Clarify doc comment on CustomHeaders: can override auth headers - Clear stale synced headers on repeated SetConfig calls to avoid leaks - Restore APP_SECRET_KEY env var fallback in ClientTest - Add ContentHeaderNames set for content-vs-request header routing Co-Authored-By: Claude --- .../Bootstrap/GeneralUpdateBootstrap.cs | 10 ++++++ .../Configuration/UpdateConfiguration.cs | 3 +- .../Network/HttpClientProvider.cs | 33 +++++++++++++++++-- tests/ClientTest/Program.cs | 4 ++- 4 files changed, 45 insertions(+), 5 deletions(-) diff --git a/src/c#/GeneralUpdate.Core/Bootstrap/GeneralUpdateBootstrap.cs b/src/c#/GeneralUpdate.Core/Bootstrap/GeneralUpdateBootstrap.cs index e479457c..544de6c2 100644 --- a/src/c#/GeneralUpdate.Core/Bootstrap/GeneralUpdateBootstrap.cs +++ b/src/c#/GeneralUpdate.Core/Bootstrap/GeneralUpdateBootstrap.cs @@ -89,6 +89,7 @@ public class GeneralUpdateBootstrap : AbstractBootstrap _syncedCustomHeaders = new(); /// /// When silent mode is active, provides access to the background polling orchestrator. @@ -201,10 +202,19 @@ public GeneralUpdateBootstrap SetConfig(UpdateRequest configInfo) // Sync custom headers to the global HTTP client so they are applied // to every request (Verification + Report) without manual extra setup. + // First clear stale headers from a previous SetConfig call, then apply + // the current ones — avoids leaking headers between bootstrap instances. + var oldHeaders = _syncedCustomHeaders; + _syncedCustomHeaders = new List(); + foreach (var key in oldHeaders) + HttpClientProvider.ExtraHeaders.TryRemove(key, out _); if (_configInfo.CustomHeaders is { Count: > 0 }) { foreach (var kv in _configInfo.CustomHeaders) + { HttpClientProvider.ExtraHeaders[kv.Key] = kv.Value; + _syncedCustomHeaders.Add(kv.Key); + } } var appType = GetOption(Option.AppType); diff --git a/src/c#/GeneralUpdate.Core/Configuration/UpdateConfiguration.cs b/src/c#/GeneralUpdate.Core/Configuration/UpdateConfiguration.cs index 7cb42083..f76eb71b 100644 --- a/src/c#/GeneralUpdate.Core/Configuration/UpdateConfiguration.cs +++ b/src/c#/GeneralUpdate.Core/Configuration/UpdateConfiguration.cs @@ -236,13 +236,14 @@ public abstract class UpdateConfiguration /// Custom HTTP headers to include in every request made by the update framework /// (both version validation and status reporting). /// Headers are applied after the authentication provider, so they can supplement - /// but not override authentication headers. + /// or override authentication headers if needed. /// /// /// Example: add ["X-Tenant-Id"] = "default-tenant" for multi-tenant servers. /// Headers set here are synced to /// at configuration time. /// + /// public Dictionary CustomHeaders { get; set; } } } diff --git a/src/c#/GeneralUpdate.Core/Network/HttpClientProvider.cs b/src/c#/GeneralUpdate.Core/Network/HttpClientProvider.cs index d623992a..d2bca7b9 100644 --- a/src/c#/GeneralUpdate.Core/Network/HttpClientProvider.cs +++ b/src/c#/GeneralUpdate.Core/Network/HttpClientProvider.cs @@ -1,6 +1,6 @@ using System; using System.Collections.Concurrent; -using System.Linq; +using System.Collections.Generic; using System.Net.Http; using System.Net.Security; using System.Security.Cryptography.X509Certificates; @@ -25,6 +25,7 @@ namespace GeneralUpdate.Core.Network; /// The global set via is /// applied by so that components like /// participate in the same authentication scheme as . +/// Extra headers set via are applied after the auth provider. /// /// public static class HttpClientProvider @@ -33,6 +34,15 @@ public static class HttpClientProvider private static IHttpAuthProvider? _defaultAuthProvider; private static readonly HttpClient _shared; + // Headers that belong on HttpContentMessage.Headers, not HttpRequestHeaders. + // Setting them via request.Headers throws InvalidOperationException. + private static readonly HashSet ContentHeaderNames = new(StringComparer.OrdinalIgnoreCase) + { + "Content-Type", "Content-Length", "Content-Encoding", "Content-Language", + "Content-Location", "Content-Disposition", "Content-Range", "Content-MD5", + "Last-Modified", "Expires", "Allow" + }; + static HttpClientProvider() { var handler = new HttpClientHandler(); @@ -104,6 +114,15 @@ public static IHttpAuthProvider? DefaultAuthProvider /// to the specified request. When no global provider is configured, /// the auth step is a no-op. /// + /// + /// + /// Extra headers (see ) are applied after the auth provider, + /// so they can supplement authentication headers and also appear on their own when + /// no auth provider is configured. If an extra header key matches a content header + /// (e.g. Content-Type), it is set on request.Content.Headers instead + /// of request.Headers to avoid . + /// + /// /// The HTTP request message to authenticate. /// A cancellation token for the authentication operation. /// A task representing the asynchronous operation. @@ -114,8 +133,16 @@ public static async Task ApplyAuthAsync(HttpRequestMessage request, Cancellation foreach (var kv in ExtraHeaders) { - request.Headers.Remove(kv.Key); - request.Headers.Add(kv.Key, kv.Value); + if (ContentHeaderNames.Contains(kv.Key) && request.Content != null) + { + request.Content.Headers.Remove(kv.Key); + request.Content.Headers.TryAddWithoutValidation(kv.Key, kv.Value); + } + else + { + request.Headers.Remove(kv.Key); + request.Headers.Add(kv.Key, kv.Value); + } } } } diff --git a/tests/ClientTest/Program.cs b/tests/ClientTest/Program.cs index f0772482..7a3dba76 100644 --- a/tests/ClientTest/Program.cs +++ b/tests/ClientTest/Program.cs @@ -45,7 +45,9 @@ static async Task RunUpdateTestAsync() var updateUrl = "http://localhost:7391/Upgrade/Verification"; var reportUrl = "http://localhost:7391/Upgrade/Report"; - var appSecretKey = "dfeb5833-975e-4afb-88f1-6278ee9aeff6"; + var appSecretKey = + Environment.GetEnvironmentVariable("APP_SECRET_KEY") + ?? "dfeb5833-975e-4afb-88f1-6278ee9aeff6"; Console.WriteLine($"UpdateUrl: {updateUrl}"); Console.WriteLine($"ReportUrl: {reportUrl}"); From b6587d86bf45201e1e3558cb4e6ed9b8323231d1 Mon Sep 17 00:00:00 2001 From: JusterZhu Date: Sat, 20 Jun 2026 01:16:21 +0800 Subject: [PATCH 3/3] fix CI: add missing using System.Collections.Generic for netstandard2.0 Co-Authored-By: Claude --- .../Bootstrap/GeneralUpdateBootstrap.cs | 13 ++++--------- 1 file changed, 4 insertions(+), 9 deletions(-) diff --git a/src/c#/GeneralUpdate.Core/Bootstrap/GeneralUpdateBootstrap.cs b/src/c#/GeneralUpdate.Core/Bootstrap/GeneralUpdateBootstrap.cs index 544de6c2..3c5b270e 100644 --- a/src/c#/GeneralUpdate.Core/Bootstrap/GeneralUpdateBootstrap.cs +++ b/src/c#/GeneralUpdate.Core/Bootstrap/GeneralUpdateBootstrap.cs @@ -1,4 +1,5 @@ using System; +using System.Collections.Generic; using System.Diagnostics; using System.IO; using System.Linq; @@ -516,15 +517,9 @@ private void ConfigureStrategy(IStrategy roleStrategy) var downloadExecutor = ResolveExtension(); Func? downloadPipelineFactory = null; - var pipelineType = ResolveExtensionType(); - if (pipelineType != null) - { - var stringCtor = pipelineType.GetConstructor([typeof(string)]); - if (stringCtor != null) - downloadPipelineFactory = hash => (Download.Abstractions.IDownloadPipeline)stringCtor.Invoke([hash]); - else - downloadPipelineFactory = _ => (Download.Abstractions.IDownloadPipeline)Activator.CreateInstance(pipelineType); - } + var downloadPipeline = ResolveExtension(); + if (downloadPipeline != null) + downloadPipelineFactory = _ => downloadPipeline; switch (roleStrategy) {