config(battle-node): require NodeServerUrl from appsettings, no fallback

Move the localhost default off BattleNodeOptions and out of Program.cs
into an appsettings.json "BattleNode" section, and make AddBattleNode
throw at startup if the value is missing or blank. Deployments now must
set the URL explicitly, and dev keeps working via the checked-in value.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
This commit is contained in:
gamer147
2026-07-04 15:10:23 -04:00
parent 6b0b467f31
commit 1bdcd8397b
4 changed files with 43 additions and 13 deletions

View File

@@ -1,13 +1,25 @@
namespace SVSim.BattleNode.Bridge; namespace SVSim.BattleNode.Bridge;
/// <summary> /// <summary>
/// DI-injected options for the battle node. NodeServerUrl matches the prod /// DI-injected options for the battle node.
/// do_matching wire format: <c>host:port/socket.io/</c>, no scheme prefix.
/// BestHTTP's SocketManager parses it as the Socket.IO v2 endpoint URL.
/// </summary> /// </summary>
public sealed class BattleNodeOptions public sealed class BattleNodeOptions
{ {
public string NodeServerUrl { get; set; } = "localhost:5148/socket.io/"; /// <summary>
/// The Socket.IO v2 endpoint URL echoed back to the client on <c>/*/do_matching</c>
/// success (matching_state 3004/3007/3011). Matches the prod do_matching wire format:
/// <c>host[:port]/socket.io/</c> — no scheme prefix, must end with <c>/socket.io/</c>.
/// The client's BestHTTP <c>SocketManager</c> parses this string directly; a leading
/// <c>http://</c>/<c>https://</c> or a missing trailing slash will make the client
/// fail to connect. Host may be an IP, hostname, or FQDN.
/// <para>
/// Deployment-time value — no hardcoded fallback. Must be provided via the
/// <c>"BattleNode:NodeServerUrl"</c> key in <c>appsettings.json</c> (or an
/// equivalently-named env var); <see cref="Hosting.BattleNodeExtensions.AddBattleNode"/>
/// validates presence at startup and throws if empty.
/// </para>
/// </summary>
public string NodeServerUrl { get; set; } = "";
/// <summary> /// <summary>
/// How long the first arriver's WS waits for a partner before disconnecting. /// How long the first arriver's WS waits for a partner before disconnecting.

View File

@@ -18,15 +18,26 @@ public static class BattleNodeExtensions
/// instance the WebSocket handler constructs on connect. /// instance the WebSocket handler constructs on connect.
/// </summary> /// </summary>
/// <param name="configure"> /// <param name="configure">
/// Optional callback to override <see cref="BattleNodeOptions"/> defaults. The default /// Callback to populate <see cref="BattleNodeOptions"/>. Must set
/// <c>NodeServerUrl</c> assumes the EmulatedEntrypoint host on /// <see cref="BattleNodeOptions.NodeServerUrl"/> — there is no hardcoded fallback; the
/// <c>http://localhost:5148</c> and shares the port for the Socket.IO endpoint. Override /// value is a deployment concern (localhost during dev, a real host[:port]/socket.io/
/// when the node runs on a different port/host or behind a reverse proxy. /// when the node runs behind a reverse proxy or on a separate box). Startup throws
/// <see cref="InvalidOperationException"/> if it's still empty after the callback runs.
/// </param> /// </param>
/// <exception cref="InvalidOperationException">
/// <see cref="BattleNodeOptions.NodeServerUrl"/> is null, empty, or whitespace after
/// <paramref name="configure"/> runs.
/// </exception>
public static IServiceCollection AddBattleNode(this IServiceCollection services, Action<BattleNodeOptions>? configure = null) public static IServiceCollection AddBattleNode(this IServiceCollection services, Action<BattleNodeOptions>? configure = null)
{ {
var options = new BattleNodeOptions(); var options = new BattleNodeOptions();
configure?.Invoke(options); configure?.Invoke(options);
if (string.IsNullOrWhiteSpace(options.NodeServerUrl))
{
throw new InvalidOperationException(
"BattleNode:NodeServerUrl is not configured. Set it in appsettings.json under " +
"the \"BattleNode\" section (format: \"host[:port]/socket.io/\", no scheme prefix).");
}
services.AddSingleton(options); services.AddSingleton(options);
services.AddSingleton<IBattleSessionStore, InMemoryBattleSessionStore>(); services.AddSingleton<IBattleSessionStore, InMemoryBattleSessionStore>();
services.AddSingleton<IMatchingBridge, MatchingBridge>(); services.AddSingleton<IMatchingBridge, MatchingBridge>();

View File

@@ -165,11 +165,10 @@ public class Program
builder.Services.AddBattleNode(opt => builder.Services.AddBattleNode(opt =>
{ {
// Matches the prod do_matching wire format: host:port/socket.io/, no scheme prefix. // Every field on BattleNodeOptions is populated from the "BattleNode" section in
// BestHTTP's SocketManager parses this as the Socket.IO v2 endpoint URL. // appsettings*.json. NodeServerUrl has no hardcoded fallback — AddBattleNode
opt.NodeServerUrl = "localhost:5148/socket.io/"; // throws at startup if it's missing/blank. See BattleNodeOptions.NodeServerUrl
// Any field in BattleNodeOptions can be overridden via the "BattleNode" section // for the required wire format.
// in appsettings*.json — see appsettings.Development.json for DiagnosticLogging.
builder.Configuration.GetSection("BattleNode").Bind(opt); builder.Configuration.GetSection("BattleNode").Bind(opt);
}); });
// In-process FCFS pair-up for TK2 PvP /do_matching, plus rank-battle's AI-fallback // In-process FCFS pair-up for TK2 PvP /do_matching, plus rank-battle's AI-fallback

View File

@@ -27,5 +27,13 @@
"Deck": { "Deck": {
"MaxDeckSlots": 36 "MaxDeckSlots": 36
}, },
"BattleNode": {
// Socket.IO v2 endpoint URL echoed to the client on /*/do_matching success.
// Wire format: "host[:port]/socket.io/" — no scheme prefix, trailing slash REQUIRED.
// Host may be an IP, hostname, or FQDN. BestHTTP's SocketManager parses this string
// directly; a leading http:// or https:// will make the client fail to connect.
// Startup throws if this value is missing or blank; there is no hardcoded fallback.
"NodeServerUrl": "localhost:5148/socket.io/"
},
"AllowedHosts": "*" "AllowedHosts": "*"
} }