Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
15 changes: 15 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,21 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0

## [Unreleased]

## [1.2.2] - 2026-08-11

### Fixed

#### NoaaClient Package
- Treat AWS WAF `202` challenges (`x-amzn-waf-action: challenge`) and empty response bodies as failures via `EnsureNoaaSuccessAsync` for all NOAA clients (`Rtsw`, `Ace`, `KpIndex`, `WsaEnlil`)
- Sanitize bare/quoted `NaN`/`Infinity` literals in RTSW JSON before deserialization (lazy allocation when the payload is clean)

### Changed

#### Dependencies
- Bump `AuroraScienceHub.Framework.Http` and `AuroraScienceHub.Framework.Utilities` from 10.0.5 to 10.0.7
- Bump `Microsoft.Extensions.*` pins (`DependencyInjection.Abstractions`, `Hosting`, `Http`, `Logging.Abstractions`) from 10.0.7 to 10.0.10 (required by Framework 10.0.7)
- Dual-map `AuroraScienceHub.*` to nuget.org in `NuGet.Config` so stable Framework packages resolve from nuget.org

## [1.1.1] - 2026-07-07

### Fixed
Expand Down
2 changes: 1 addition & 1 deletion Directory.Build.props
Original file line number Diff line number Diff line change
Expand Up @@ -29,7 +29,7 @@
</PropertyGroup>

<PropertyGroup>
<PackageBaseVersion>1.2.0</PackageBaseVersion>
<PackageBaseVersion>1.2.2</PackageBaseVersion>
<MinVerTagPrefix></MinVerTagPrefix>
Comment thread
Demosfen marked this conversation as resolved.
</PropertyGroup>

Expand Down
11 changes: 6 additions & 5 deletions Directory.Packages.props
Original file line number Diff line number Diff line change
Expand Up @@ -5,8 +5,8 @@
<EnablePackageVersionOverride>true</EnablePackageVersionOverride>
</PropertyGroup>
<ItemGroup>
<PackageVersion Include="AuroraScienceHub.Framework.Http" Version="10.0.5" />
<PackageVersion Include="AuroraScienceHub.Framework.Utilities" Version="10.0.5" />
<PackageVersion Include="AuroraScienceHub.Framework.Http" Version="10.0.7" />
<PackageVersion Include="AuroraScienceHub.Framework.Utilities" Version="10.0.7" />
<PackageVersion Include="BenchmarkDotNet" Version="0.15.8" />
<PackageVersion Include="coverlet.collector" Version="6.0.4">
<PrivateAssets>all</PrivateAssets>
Expand All @@ -16,9 +16,10 @@
<PackageVersion Include="Microsoft.CodeAnalysis.Analyzers" Version="3.11.0" />
<PackageVersion Include="Microsoft.CodeAnalysis.Workspaces.Common" Version="4.14.0" />
<PackageVersion Include="Microsoft.CodeAnalysis.Workspaces.MSBuild" Version="4.14.0" />
<PackageVersion Include="Microsoft.Extensions.DependencyInjection.Abstractions" Version="10.0.7" />
<PackageVersion Include="Microsoft.Extensions.Hosting" Version="10.0.7" />
<PackageVersion Include="Microsoft.Extensions.Http" Version="10.0.7" />
<PackageVersion Include="Microsoft.Extensions.DependencyInjection.Abstractions" Version="10.0.10" />
<PackageVersion Include="Microsoft.Extensions.Hosting" Version="10.0.10" />
<PackageVersion Include="Microsoft.Extensions.Http" Version="10.0.10" />
<PackageVersion Include="Microsoft.Extensions.Logging.Abstractions" Version="10.0.10" />
<PackageVersion Include="FFMpegCore" Version="5.4.0" />
<PackageVersion Include="MinVer" Version="6.0.0" />
<PackageVersion Include="Microsoft.NET.Test.Sdk" Version="18.0.1" />
Expand Down
9 changes: 5 additions & 4 deletions NuGet.Config
Original file line number Diff line number Diff line change
Expand Up @@ -2,18 +2,19 @@
<configuration>
<packageSources>
<clear />
<!-- Primary source -->
<!-- nuget.org first: stable packages including AuroraScienceHub.* -->
<add key="nuget.org" value="https://api.nuget.org/v3/index.json" />
<!-- Fallback source for packages unavailable on nuget.org -->
<!-- GitHub Packages: AuroraScienceHub.* prereleases / versions not on nuget.org -->
<add key="AuroraScienceHub" value="https://nuget.pkg.github.com/Aurora-Science-Hub/index.json" />
</packageSources>
<packageSourceMapping>
<!-- Mapping is by package ID, not version kind. Dual-map AuroraScienceHub.* so
exact stable pins resolve from nuget.org; exact prerelease pins from GH Packages. -->
<packageSource key="nuget.org">
<!-- Current repository packages resolve from nuget.org -->
<package pattern="AuroraScienceHub.*" />
<package pattern="*" />
</packageSource>
<packageSource key="AuroraScienceHub">
<!-- Explicit opt-in for future GitHub-only packages -->
<package pattern="AuroraScienceHub.*" />
</packageSource>
</packageSourceMapping>
Expand Down
9 changes: 5 additions & 4 deletions src/NoaaClient/Ace/AceClient.cs
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
using AuroraScienceHub.Integrations.NoaaClient.Ace.Extensions;
using AuroraScienceHub.Integrations.NoaaClient.Ace.Responses;
using AuroraScienceHub.Integrations.NoaaClient.Http;
using Microsoft.Extensions.Options;

namespace AuroraScienceHub.Integrations.NoaaClient.Ace;
Expand All @@ -23,8 +24,8 @@ public AceClient(
public async Task<IReadOnlyList<MagnetometerRecord>> GetMagnetometerDataAsync(CancellationToken cancellationToken)
{
var url = new Uri(_baseUrl, "text/ace-magnetometer.txt");
var response = await _httpClient.GetAsync(url, cancellationToken).ConfigureAwait(false);
response.EnsureSuccessStatusCode();
using var response = await _httpClient.GetAsync(url, cancellationToken).ConfigureAwait(false);
await response.EnsureNoaaSuccessAsync(url, cancellationToken).ConfigureAwait(false);
var text = await response.Content.ReadAsStringAsync(cancellationToken).ConfigureAwait(false);

return MagnetometerDataParser.Parse(text);
Expand All @@ -33,8 +34,8 @@ public async Task<IReadOnlyList<MagnetometerRecord>> GetMagnetometerDataAsync(Ca
public async Task<IReadOnlyList<SolarWindPlasmaRecord>> GetSwepamDataAsync(CancellationToken cancellationToken)
{
var url = new Uri(_baseUrl, "text/ace-swepam.txt");
var response = await _httpClient.GetAsync(url, cancellationToken).ConfigureAwait(false);
response.EnsureSuccessStatusCode();
using var response = await _httpClient.GetAsync(url, cancellationToken).ConfigureAwait(false);
await response.EnsureNoaaSuccessAsync(url, cancellationToken).ConfigureAwait(false);
var text = await response.Content.ReadAsStringAsync(cancellationToken).ConfigureAwait(false);

return SolarWindPlasmaDataParser.Parse(text);
Expand Down
76 changes: 76 additions & 0 deletions src/NoaaClient/Http/NoaaHttpResponseExtensions.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,76 @@
using System.Net;
using AuroraScienceHub.Framework.Http;

namespace AuroraScienceHub.Integrations.NoaaClient.Http;

/// <summary>
/// NOAA-specific HTTP response validation helpers.
/// </summary>
internal static class NoaaHttpResponseExtensions
{
private const string WafActionHeaderName = "x-amzn-waf-action";
private const string WafChallengeAction = "challenge";

/// <summary>
/// Ensures the NOAA response is not a WAF challenge and has a successful status code.
/// </summary>
public static async Task EnsureNoaaSuccessAsync(
this HttpResponseMessage response,
Uri? requestUri = null,
CancellationToken cancellationToken = default)
{
cancellationToken.ThrowIfCancellationRequested();
ThrowIfWafChallenge(response, requestUri);
await response.EnsureSuccess().ConfigureAwait(false);
}

/// <summary>
/// Throws when the NOAA response body is empty after a successful status code.
/// </summary>
public static void ThrowIfEmptyBody(
this HttpResponseMessage response,
ReadOnlySpan<byte> body,
Uri? requestUri = null)
{
if (body.Length > 0)
{
return;
}

var contentLength = response.Content.Headers.ContentLength;
throw new HttpRequestException(
$"NOAA returned an empty response body (HTTP {(int)response.StatusCode}, ContentLength={contentLength}, Uri={requestUri}).",
inner: null,
statusCode: response.StatusCode);
}

internal static string? GetWafAction(HttpResponseMessage response)
{
if (response.Headers.TryGetValues(WafActionHeaderName, out var values))
{
return values.FirstOrDefault();
}

if (response.Content.Headers.TryGetValues(WafActionHeaderName, out values))
{
return values.FirstOrDefault();
}

return null;
}

private static void ThrowIfWafChallenge(HttpResponseMessage response, Uri? requestUri)
{
var wafAction = GetWafAction(response);
if (response.StatusCode != HttpStatusCode.Accepted
|| !string.Equals(wafAction, WafChallengeAction, StringComparison.OrdinalIgnoreCase))
{
return;
}

throw new HttpRequestException(
$"NOAA request blocked by AWS WAF challenge (HTTP {(int)response.StatusCode}, {WafActionHeaderName}={wafAction}, Uri={requestUri}).",
inner: null,
statusCode: response.StatusCode);
}
}
5 changes: 3 additions & 2 deletions src/NoaaClient/KpIndex/KpIndexClient.cs
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
using AuroraScienceHub.Integrations.NoaaClient.Http;
using AuroraScienceHub.Integrations.NoaaClient.KpIndex.Extensions;
using AuroraScienceHub.Integrations.NoaaClient.KpIndex.Responses;
using Microsoft.Extensions.Options;
Expand Down Expand Up @@ -41,8 +42,8 @@ public async Task<IReadOnlyList<KpIndexNowcastResponse>> GetKpIndexNowcastAsync(

private async Task<string?> GetStringOrDefaultAsync(Uri url, CancellationToken cancellationToken)
{
var response = await _client.GetAsync(url, cancellationToken).ConfigureAwait(false);
response.EnsureSuccessStatusCode();
using var response = await _client.GetAsync(url, cancellationToken).ConfigureAwait(false);
await response.EnsureNoaaSuccessAsync(url, cancellationToken).ConfigureAwait(false);

return await response.Content.ReadAsStringAsync(cancellationToken).ConfigureAwait(false);
}
Expand Down
1 change: 1 addition & 0 deletions src/NoaaClient/NoaaClient.csproj
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@
<PackageReference Include="AuroraScienceHub.Framework.Http" />
<PackageReference Include="AuroraScienceHub.Framework.Utilities" />
<PackageReference Include="Microsoft.Extensions.DependencyInjection.Abstractions" />
<PackageReference Include="Microsoft.Extensions.Logging.Abstractions" />
<PackageReference Include="FFMpegCore" />
<PackageReference Include="Microsoft.Extensions.Http" />
</ItemGroup>
Expand Down
71 changes: 62 additions & 9 deletions src/NoaaClient/Rtsw/RtswClient.cs
Original file line number Diff line number Diff line change
@@ -1,43 +1,96 @@
using System.Net;
using System.Text.Json;
using AuroraScienceHub.Framework.Http;
using AuroraScienceHub.Framework.Json;
using AuroraScienceHub.Integrations.NoaaClient.Http;
using AuroraScienceHub.Integrations.NoaaClient.Rtsw.Responses;
using AuroraScienceHub.Integrations.NoaaClient.Utilities;
using Microsoft.Extensions.Logging;
using Microsoft.Extensions.Options;

namespace AuroraScienceHub.Integrations.NoaaClient.Rtsw;

/// <inheritdoc />
internal sealed class RtswClient : IRtswClient
{
private static readonly JsonSerializerOptions s_jsonOptions = DefaultJsonSerializerOptions.Create();

private readonly HttpClient _httpClient;
private readonly Uri _baseUrl;
private readonly ILogger _logger;

/// <summary>
/// Initializes a new instance of the <see cref="RtswClient"/> class.
/// </summary>
public RtswClient(
HttpClient httpClient,
IOptions<NoaaClientOptions> options)
IOptions<NoaaClientOptions> options,
ILogger<RtswClient> logger)
{
_httpClient = httpClient;
_baseUrl = options.Value.RequiredServerUrl;
_logger = logger;
}

public async Task<IReadOnlyList<MagnetometerRecord>> GetMagnetometerDataAsync(CancellationToken cancellationToken)
{
var url = new Uri(_baseUrl, "json/rtsw/rtsw_mag_1m.json");
var records = await _httpClient
.GetFromJsonOrDefaultAsync<List<MagnetometerRecord>>(url, cancellationToken)
.ConfigureAwait(false);

return records ?? [];
return await GetRtswJsonAsync<List<MagnetometerRecord>>(url, cancellationToken).ConfigureAwait(false)
?? [];
}

public async Task<IReadOnlyList<SolarWindPlasmaRecord>> GetSolarWindPlasmaDataAsync(CancellationToken cancellationToken)
{
var url = new Uri(_baseUrl, "json/rtsw/rtsw_wind_1m.json");
var records = await _httpClient
.GetFromJsonOrDefaultAsync<List<SolarWindPlasmaRecord>>(url, cancellationToken)
return await GetRtswJsonAsync<List<SolarWindPlasmaRecord>>(url, cancellationToken).ConfigureAwait(false)
?? [];
}

private async Task<TResponse?> GetRtswJsonAsync<TResponse>(
Uri requestUri,
CancellationToken cancellationToken)
{
using var response = await _httpClient.GetAsync(requestUri, cancellationToken).ConfigureAwait(false);
return await ReadRtswJsonOrThrowAsync<TResponse>(response, requestUri, cancellationToken).ConfigureAwait(false);
}

private async Task<TResponse?> ReadRtswJsonOrThrowAsync<TResponse>(
HttpResponseMessage response,
Uri requestUri,
CancellationToken cancellationToken)
{
var wafAction = NoaaHttpResponseExtensions.GetWafAction(response);
var contentLength = response.Content.Headers.ContentLength;

_logger.LogInformation(
"RTSW HTTP response Uri={RequestUri} StatusCode={StatusCode} WafAction={WafAction} ContentLength={ContentLength}",
requestUri,
(int)response.StatusCode,
wafAction,
contentLength);

await response.EnsureNoaaSuccessAsync(requestUri, cancellationToken).ConfigureAwait(false);

if (response.StatusCode == HttpStatusCode.NoContent)
{
response.ThrowIfEmptyBody(ReadOnlySpan<byte>.Empty, requestUri);
}

var rawBytes = await response.Content
.ReadAsByteArrayAsync(cancellationToken)
.ConfigureAwait(false);

return records ?? [];
response.ThrowIfEmptyBody(rawBytes, requestUri);

var sanitized = NoaaJsonSanitizer.Sanitize(rawBytes);
if (sanitized.ReplacementCount > 0)
{
_logger.LogWarning(
"NOAA RTSW JSON sanitized {ReplacementCount} non-standard numeric literals to null. Uri={RequestUri}",
sanitized.ReplacementCount,
requestUri);
}

return JsonSerializer.Deserialize<TResponse>(sanitized.Bytes.Span, s_jsonOptions);
}
}
Loading
Loading