diff --git a/CHANGELOG.md b/CHANGELOG.md
index e4ae874..8ea9e85 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -7,6 +7,18 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
## [Unreleased]
+## [1.3.0] - 2026-08-15
+
+### Added
+
+#### NoaaClient Package
+- New `NoaaClientOptions.UseProxy` configuration option to route NOAA client requests through a proxy (configured in the `Proxy` section) via `AuroraScienceHub.Framework.Http.Proxy`
+
+### Changed
+
+#### NoaaClient Package
+- **Breaking:** `AddNoaaClients()` now requires an `IConfiguration` argument (`AddNoaaClients(configuration)`); `NoaaClientOptions` and `ProxyOptions` are bound directly from the provided configuration
+
## [1.2.2] - 2026-08-11
### Fixed
@@ -105,8 +117,10 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
- Embedded debug symbols in NuGet packages
- GitHub Actions CI/CD pipeline for build and test
+[1.3.0]: https://github.com/Aurora-Science-Hub/Integrations/compare/1.2.2...1.3.0
+[1.2.2]: https://github.com/Aurora-Science-Hub/Integrations/compare/1.1.1...1.2.2
[1.1.1]: https://github.com/Aurora-Science-Hub/Integrations/compare/1.1.0...1.1.1
[1.1.0]: https://github.com/Aurora-Science-Hub/Integrations/compare/1.0.0...1.1.0
[1.0.0]: https://github.com/Aurora-Science-Hub/Integrations/releases/tag/1.0.0
-[Unreleased]: https://github.com/Aurora-Science-Hub/Integrations/compare/1.1.1...HEAD
+[Unreleased]: https://github.com/Aurora-Science-Hub/Integrations/compare/1.3.0...HEAD
diff --git a/Directory.Build.props b/Directory.Build.props
index a2419b6..3a98c7b 100644
--- a/Directory.Build.props
+++ b/Directory.Build.props
@@ -29,7 +29,7 @@
- 1.2.2
+ 1.3.0
diff --git a/README.md b/README.md
index bd748d5..5210ae8 100644
--- a/README.md
+++ b/README.md
@@ -47,7 +47,7 @@ dotnet add package AuroraScienceHub.Integrations.NoaaClient
```csharp
// Register services
-builder.Services.AddNoaaClients();
+builder.Services.AddNoaaClients(builder.Configuration);
// Inject and use clients
public class SpaceWeatherService
diff --git a/samples/NoaaClientSample/Program.cs b/samples/NoaaClientSample/Program.cs
index 86629ae..ba69a49 100644
--- a/samples/NoaaClientSample/Program.cs
+++ b/samples/NoaaClientSample/Program.cs
@@ -14,7 +14,7 @@
// Setup DI and configuration
var builder = Host.CreateApplicationBuilder(args);
builder.Configuration.AddJsonFile("appsettings.json", optional: false);
-builder.Services.AddNoaaClients();
+builder.Services.AddNoaaClients(builder.Configuration);
var host = builder.Build();
// Get NOAA clients from DI
diff --git a/src/NoaaClient/NoaaClientOptions.cs b/src/NoaaClient/NoaaClientOptions.cs
index c30e1f4..7ee384d 100644
--- a/src/NoaaClient/NoaaClientOptions.cs
+++ b/src/NoaaClient/NoaaClientOptions.cs
@@ -17,6 +17,11 @@ public sealed class NoaaClientOptions
///
public Uri? ServerUrl { get; set; }
+ ///
+ /// Use a proxy
+ ///
+ public bool UseProxy { get; set; } = false;
+
///
/// Gets the required server URL. Throws if not set.
///
diff --git a/src/NoaaClient/README.md b/src/NoaaClient/README.md
index 1789b96..6f5bb33 100644
--- a/src/NoaaClient/README.md
+++ b/src/NoaaClient/README.md
@@ -17,15 +17,26 @@ dotnet add package AuroraScienceHub.Integrations.NoaaClient
```json
{
"Noaa": {
- "ServerUrl": "https://services.swpc.noaa.gov"
+ "ServerUrl": "https://services.swpc.noaa.gov",
+ "UseProxy": false
+ },
+ "Proxy": {
+ "Address": "http://proxy.example.com:8080",
+ "UserName": "",
+ "Password": ""
}
}
```
+- **Noaa:ServerUrl** — NOAA SWPC base URL (required).
+- **Noaa:UseProxy** — when `true`, client requests are routed through the proxy configured in the `Proxy` section. Default: `false`.
+- **Proxy:Address** — proxy server URI (required when `UseProxy` is enabled; startup fails with `InvalidOperationException` if missing).
+- **Proxy:UserName** / **Proxy:Password** — optional proxy credentials. When omitted, default credentials are used.
+
Register clients in DI:
```csharp
-builder.Services.AddNoaaClients();
+builder.Services.AddNoaaClients(builder.Configuration);
```
## RTSW Client
diff --git a/src/NoaaClient/ServiceCollectionExtensions.cs b/src/NoaaClient/ServiceCollectionExtensions.cs
index bab307a..5b92a30 100644
--- a/src/NoaaClient/ServiceCollectionExtensions.cs
+++ b/src/NoaaClient/ServiceCollectionExtensions.cs
@@ -1,7 +1,9 @@
+using AuroraScienceHub.Framework.Http.Proxy;
using AuroraScienceHub.Integrations.NoaaClient.Ace;
-using AuroraScienceHub.Integrations.NoaaClient.WsaEnlil;
using AuroraScienceHub.Integrations.NoaaClient.KpIndex;
using AuroraScienceHub.Integrations.NoaaClient.Rtsw;
+using AuroraScienceHub.Integrations.NoaaClient.WsaEnlil;
+using Microsoft.Extensions.Configuration;
using Microsoft.Extensions.DependencyInjection;
namespace AuroraScienceHub.Integrations.NoaaClient;
@@ -14,21 +16,50 @@ public static class ServiceCollectionExtensions
///
/// Adds NOAA clients to the service collection.
///
+ /// The to add services to.
+ /// The application configuration used to read the section.
///
/// Registers for backward compatibility only; prefer .
+ ///
+ /// When is enabled in configuration, client requests are routed
+ /// through the proxy configured in the Proxy section (see ).
+ ///
///
- public static IServiceCollection AddNoaaClients(this IServiceCollection services)
+ public static IServiceCollection AddNoaaClients(
+ this IServiceCollection services,
+ IConfiguration configuration)
{
services.AddOptions()
- .BindConfiguration(NoaaClientOptions.OptionKey);
+ .Bind(configuration.GetSection(NoaaClientOptions.OptionKey));
+
+ services.AddOptions()
+ .Bind(configuration.GetSection(ProxyOptions.OptionKey));
+
+ var useProxy = configuration
+ .GetSection(NoaaClientOptions.OptionKey)
+ .GetValue(nameof(NoaaClientOptions.UseProxy));
#pragma warning disable CS0618 // ACE client registration pending removal in issue #3.
- services.AddHttpClient();
+ services.AddNoaaHttpClient(useProxy);
#pragma warning restore CS0618
- services.AddHttpClient();
- services.AddHttpClient();
- services.AddHttpClient();
+ services.AddNoaaHttpClient(useProxy);
+ services.AddNoaaHttpClient(useProxy);
+ services.AddNoaaHttpClient(useProxy);
return services;
}
+
+ private static void AddNoaaHttpClient(
+ this IServiceCollection services,
+ bool useProxy)
+ where TClient : class
+ where TImplementation : class, TClient
+ {
+ var builder = services.AddHttpClient();
+
+ if (useProxy)
+ {
+ builder.ConfigurePrimaryHttpProxyMessageHandler();
+ }
+ }
}
diff --git a/tests/UnitTests/NoaaClient/ServiceCollectionExtensionsTests.cs b/tests/UnitTests/NoaaClient/ServiceCollectionExtensionsTests.cs
new file mode 100644
index 0000000..6f2ceaf
--- /dev/null
+++ b/tests/UnitTests/NoaaClient/ServiceCollectionExtensionsTests.cs
@@ -0,0 +1,142 @@
+using System.Net;
+using System.Reflection;
+using AuroraScienceHub.Integrations.NoaaClient;
+using AuroraScienceHub.Integrations.NoaaClient.Rtsw;
+using Microsoft.Extensions.Configuration;
+using Microsoft.Extensions.DependencyInjection;
+using Microsoft.Extensions.Http;
+using Shouldly;
+
+namespace AuroraScienceHub.Integrations.UnitTests.NoaaClient;
+
+///
+/// Tests for proxy configuration.
+///
+public sealed class ServiceCollectionExtensionsTests
+{
+ private static readonly Uri ProxyAddress = new("http://proxy.example.com:8080");
+
+ [Fact(DisplayName = "Noaa clients do not use a custom proxy when UseProxy is not enabled")]
+ public void AddNoaaClients_WhenUseProxyDisabled_ConfiguresClientWithoutProxy()
+ {
+ // Arrange / Act
+ var (_, proxy) = CreatePrimaryHandlerProxy(CreateConfiguration(useProxy: false), typeof(IRtswClient));
+
+ // Assert
+ proxy.ShouldBeNull();
+ }
+
+ [Fact(DisplayName = "Noaa clients route through the configured proxy when UseProxy is enabled")]
+ public void AddNoaaClients_WhenUseProxyEnabled_ConfiguresProxyAddress()
+ {
+ // Arrange / Act
+ var (useProxy, proxy) = CreatePrimaryHandlerProxy(
+ CreateConfiguration(useProxy: true, address: ProxyAddress),
+ typeof(IRtswClient));
+
+ // Assert
+ useProxy.ShouldBeTrue();
+ var webProxy = proxy.ShouldBeOfType();
+ webProxy.Address.ShouldBe(ProxyAddress);
+ }
+
+ [Fact(DisplayName = "Noaa clients apply proxy credentials when configured")]
+ public void AddNoaaClients_WhenProxyCredentialsConfigured_AppliesCredentials()
+ {
+ // Arrange / Act
+ var (_, proxy) = CreatePrimaryHandlerProxy(
+ CreateConfiguration(useProxy: true, address: ProxyAddress, userName: "user", password: "pass"),
+ typeof(IRtswClient));
+
+ // Assert
+ var webProxy = proxy.ShouldBeOfType();
+ var credentials = webProxy.Credentials.ShouldBeOfType();
+ credentials.UserName.ShouldBe("user");
+ credentials.Password.ShouldBe("pass");
+ webProxy.UseDefaultCredentials.ShouldBeFalse();
+ }
+
+ [Fact(DisplayName = "Noaa clients fail fast when UseProxy is enabled but the proxy address is missing")]
+ public void AddNoaaClients_WhenUseProxyEnabledWithoutAddress_ThrowsInvalidOperationException()
+ {
+ // Arrange / Act
+ var exception = Should.Throw(
+ () => CreatePrimaryHandlerProxy(CreateConfiguration(useProxy: true), typeof(IRtswClient)));
+
+ // Assert
+ exception.Message.ShouldContain("Proxy");
+ }
+
+ private static (bool UseProxy, IWebProxy? Proxy) CreatePrimaryHandlerProxy(
+ IConfiguration configuration,
+ Type clientType)
+ {
+ var services = new ServiceCollection();
+ services.AddNoaaClients(configuration);
+
+ using var provider = services.BuildServiceProvider();
+
+ var handlerFactory = provider.GetRequiredService();
+ var handler = handlerFactory.CreateHandler(clientType.Name);
+ var primaryHandler = UnwrapPrimaryHandler(handler);
+
+ return primaryHandler switch
+ {
+ HttpClientHandler httpClientHandler => (httpClientHandler.UseProxy, httpClientHandler.Proxy),
+ SocketsHttpHandler socketsHandler => (socketsHandler.UseProxy, socketsHandler.Proxy),
+ _ => throw new InvalidOperationException(
+ $"Unexpected primary handler type {primaryHandler.GetType().Name}."),
+ };
+ }
+
+ private static HttpMessageHandler UnwrapPrimaryHandler(HttpMessageHandler handler)
+ {
+ const BindingFlags flags = BindingFlags.Instance | BindingFlags.Public | BindingFlags.NonPublic;
+
+ while (true)
+ {
+ if (handler is HttpClientHandler or SocketsHttpHandler)
+ {
+ return handler;
+ }
+
+ var innerHandlerProperty = handler.GetType().GetProperty("InnerHandler", flags)
+ ?? throw new InvalidOperationException(
+ $"Cannot unwrap message handler of type {handler.GetType().Name}.");
+
+ handler = (HttpMessageHandler)innerHandlerProperty.GetValue(handler)!;
+ }
+ }
+
+ private static IConfiguration CreateConfiguration(
+ bool useProxy,
+ Uri? address = null,
+ string? userName = null,
+ string? password = null)
+ {
+ var data = new Dictionary
+ {
+ [$"{NoaaClientOptions.OptionKey}:ServerUrl"] = "https://noaa.test",
+ [$"{NoaaClientOptions.OptionKey}:UseProxy"] = useProxy.ToString(),
+ };
+
+ if (address is not null)
+ {
+ data["Proxy:Address"] = address.ToString();
+ }
+
+ if (userName is not null)
+ {
+ data["Proxy:UserName"] = userName;
+ }
+
+ if (password is not null)
+ {
+ data["Proxy:Password"] = password;
+ }
+
+ return new ConfigurationBuilder()
+ .AddInMemoryCollection(data)
+ .Build();
+ }
+}