microsoft/aspire · error · InvalidOperationException

A ConfigureRadiusInfrastructure callback removed port

Error message

A ConfigureRadiusInfrastructure callback removed port '{portName}' from container '{mapKey}'. Aspire service discovery already emitted this port ({expected.Port}) into consumer 'services__*' variables, so removing it would break cross-container calls. Remove the port change to keep service discovery consistent.

What it means

Service discovery variables already embed a container's port. A ConfigureRadiusInfrastructure callback removed the port (or set it to a non-port value), so consumers' services__* variables would point at a port that no longer exists; the publisher throws InvalidOperationException and asks the user to revert the port change.

Solutions

  1. Restore the port/endpoint on the container (same name and port value).
  2. If the port must change, change it in the app model before publishing so discovery variables are regenerated, not in a callback.
  3. Remove the port-mutating code from the callback entirely.
  4. Verify every pre-callback port in the snapshot still exists post-callback with an integer value.

Example fix

// before
infra.Callback(c => c.Resources.OfType<ContainerResource>().First(x => x.Name == "api").Ports.Remove("http"));
// after
infra.Callback(c => { /* keep ports intact */ });
Defensive patterns

Strategy: validation

Validate before calling

foreach (var (name, port) in preCallbackPorts)
{
    var c = finalContainers.FirstOrDefault(x => x.Name == containerName);
    if (c?.Ports.TryGetValue(name, out var v) != true || v is not int)
        throw new InvalidOperationException($"Port '{name}' must remain an integer on '{containerName}'.");
}

Try / catch

try { PublishAsync(model); }
catch (InvalidOperationException ex) when (ex.Message.Contains("callback removed port"))
{ log.LogError(ex, "Restore port '{Port}' on container '{Container}'.", ex.Data["port"], ex.Data["mapKey"]); }

Prevention

When it happens

Trigger: A ConfigureRadiusInfrastructure callback removes an endpoint/port (or mutates container.Ports so the entry is missing or not a port value) that existed pre-callback with a known port number used in emitted discovery variables.

Common situations: Callback 'tightening' exposed ports; swapping WithEndpoint definitions inside a callback; changing endpoint names so the port lookup misses; setting the port value to a non-integer expression.

Understand the failure class

Background: "Invalid state transition" errors: "status must be X, actually Y", "already rejected/charging/uninstalled", "cannot ... while running" — what they mean when a library rejects your call — this error's family across 31 libraries.

Related errors


AI-assisted analysis of microsoft/aspire@25830f84bd (2026-09-16). Data as JSON: /api/errors/889b8f3a09e84dbd. Report an issue: GitHub.

Appendix: source

Thrown at src/Aspire.Hosting.Radius/Publishing/RadiusInfrastructureBuilder.cs:4969

            {
                throw new InvalidOperationException(
                    $"A ConfigureRadiusInfrastructure callback removed or replaced container '{mapKey}'. Aspire " +
                    $"service discovery already emitted 'services__*' variables that address it, so dropping the " +
                    $"workload would break cross-container calls. Keep the container to keep service discovery consistent.");
            }

            // Only containers that had service ports pre-callback have a Service (`{name}-{name}`)
            // that `services__*` addresses, so the name/map-key equality is only required for them.
            // A portless baseline container or one added entirely by the callback has no service-
            // discovery contract — Radius permits its top-level name to differ from the map key — so
            // gating this check on a non-empty snapshot keeps the customization escape hatch open.
            ValidateContainerNameMatchesMapKey(container);

            foreach (var (portName, expected) in snapshot)
            {
                if (!container.Ports.TryGetValue(portName, out var portValue) || portValue.Value is not { } port)
                {
                    throw new InvalidOperationException(
                        $"A ConfigureRadiusInfrastructure callback removed port '{portName}' from container " +
                        $"'{mapKey}'. Aspire service discovery already emitted this port ({expected.Port}) into " +
                        $"consumer 'services__*' variables, so removing it would break cross-container calls. " +
                        $"Remove the port change to keep service discovery consistent.");
                }

                // Reject a non-literal port/protocol: service discovery is a fixed literal, so a
                // callback that swaps in a Bicep expression could evaluate to a different value at
                // deploy time, reintroducing exactly the mismatch this guard prevents. An
                // expression-backed BicepValue<int> reports a default LiteralValue of 0 (not null),
                // so a non-null Expression is the reliable "non-literal" signal, not the LiteralValue.
                var portValueBicep = (IBicepValue)port.ContainerPort;
                if (portValueBicep.Expression is not null || portValueBicep.LiteralValue is not int literalPort)
                {
                    throw new InvalidOperationException(
                        $"A ConfigureRadiusInfrastructure callback replaced port '{portName}' on container " +
                        $"'{mapKey}' with a non-literal Bicep expression. Aspire service discovery already emitted " +
                        $"the literal port {expected.Port} into consumer 'services__*' variables and cannot follow a " +

View on GitHub (pinned to 25830f84bd)