{"record":{"id":"889b8f3a09e84dbd","repo":"microsoft/aspire","slug":"a-configureradiusinfrastructure-callback-removed-port","errorCode":null,"errorMessage":"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.","messagePattern":"A ConfigureRadiusInfrastructure callback removed port '(.+?)' from container '(.+?)'\\. Aspire service discovery already emitted this port \\((.+?)\\) into consumer 'services__\\*' variables, so removing it would break cross-container calls\\. Remove the port change to keep service discovery consistent\\.","errorType":"exception","errorClass":"InvalidOperationException","httpStatus":null,"severity":"error","filePath":"src/Aspire.Hosting.Radius/Publishing/RadiusInfrastructureBuilder.cs","lineNumber":4969,"sourceCode":"            {\n                throw new InvalidOperationException(\n                    $\"A ConfigureRadiusInfrastructure callback removed or replaced container '{mapKey}'. Aspire \" +\n                    $\"service discovery already emitted 'services__*' variables that address it, so dropping the \" +\n                    $\"workload would break cross-container calls. Keep the container to keep service discovery consistent.\");\n            }\n\n            // Only containers that had service ports pre-callback have a Service (`{name}-{name}`)\n            // that `services__*` addresses, so the name/map-key equality is only required for them.\n            // A portless baseline container or one added entirely by the callback has no service-\n            // discovery contract — Radius permits its top-level name to differ from the map key — so\n            // gating this check on a non-empty snapshot keeps the customization escape hatch open.\n            ValidateContainerNameMatchesMapKey(container);\n\n            foreach (var (portName, expected) in snapshot)\n            {\n                if (!container.Ports.TryGetValue(portName, out var portValue) || portValue.Value is not { } port)\n                {\n                    throw new InvalidOperationException(\n                        $\"A ConfigureRadiusInfrastructure callback removed port '{portName}' from container \" +\n                        $\"'{mapKey}'. Aspire service discovery already emitted this port ({expected.Port}) into \" +\n                        $\"consumer 'services__*' variables, so removing it would break cross-container calls. \" +\n                        $\"Remove the port change to keep service discovery consistent.\");\n                }\n\n                // Reject a non-literal port/protocol: service discovery is a fixed literal, so a\n                // callback that swaps in a Bicep expression could evaluate to a different value at\n                // deploy time, reintroducing exactly the mismatch this guard prevents. An\n                // expression-backed BicepValue<int> reports a default LiteralValue of 0 (not null),\n                // so a non-null Expression is the reliable \"non-literal\" signal, not the LiteralValue.\n                var portValueBicep = (IBicepValue)port.ContainerPort;\n                if (portValueBicep.Expression is not null || portValueBicep.LiteralValue is not int literalPort)\n                {\n                    throw new InvalidOperationException(\n                        $\"A ConfigureRadiusInfrastructure callback replaced port '{portName}' on container \" +\n                        $\"'{mapKey}' with a non-literal Bicep expression. Aspire service discovery already emitted \" +\n                        $\"the literal port {expected.Port} into consumer 'services__*' variables and cannot follow a \" +","sourceCodeStart":4951,"sourceCodeEnd":4987,"githubUrl":"https://github.com/microsoft/aspire/blob/25830f84bd145686607ad00c057b3f84e2e51d43/src/Aspire.Hosting.Radius/Publishing/RadiusInfrastructureBuilder.cs#L4951-L4987","documentation":"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.","triggerScenarios":"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.","commonSituations":"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.","solutions":["Restore the port/endpoint on the container (same name and port value).","If the port must change, change it in the app model before publishing so discovery variables are regenerated, not in a callback.","Remove the port-mutating code from the callback entirely.","Verify every pre-callback port in the snapshot still exists post-callback with an integer value."],"exampleFix":"// before\ninfra.Callback(c => c.Resources.OfType<ContainerResource>().First(x => x.Name == \"api\").Ports.Remove(\"http\"));\n// after\ninfra.Callback(c => { /* keep ports intact */ });","handlingStrategy":"validation","validationCode":"foreach (var (name, port) in preCallbackPorts)\n{\n    var c = finalContainers.FirstOrDefault(x => x.Name == containerName);\n    if (c?.Ports.TryGetValue(name, out var v) != true || v is not int)\n        throw new InvalidOperationException($\"Port '{name}' must remain an integer on '{containerName}'.\");\n}","typeGuard":null,"tryCatchPattern":"try { PublishAsync(model); }\ncatch (InvalidOperationException ex) when (ex.Message.Contains(\"callback removed port\"))\n{ log.LogError(ex, \"Restore port '{Port}' on container '{Container}'.\", ex.Data[\"port\"], ex.Data[\"mapKey\"]); }","preventionTips":["Never remove or retarget existing endpoints/ports inside publish callbacks.","Change port topology in the AppHost before publishing so discovery variables regenerate.","Keep endpoint names and port numbers stable once consumers reference them.","Diff the pre/post-callback port snapshot in local publish runs."],"tags":["radius","callbacks","service-discovery","ports"],"backgroundTag":"invalid-state-transition","analyzedSha":"25830f84bd145686607ad00c057b3f84e2e51d43","analyzedAt":"2026-09-16T11:10:06.193Z","contentChangedAt":"2026-09-16T11:10:06.193Z","schemaVersion":2},"datasetVersion":"2026-09-21T04:17:39.646Z"}