aspire icon indicating copy to clipboard operation
aspire copied to clipboard

[13.0] `EndpointReference` environment variable evaluating to `http://:` (i.e. missing host & port)

Open afscrome opened this issue 2 months ago • 4 comments

Is there an existing issue for this?

  • [x] I have searched the existing issues

Describe the bug

I have a test in 9.5, in which I needed to run an in memory server on a port, and make sure a dependency connects to it. (Specifically the otel connector)

After upgrading to 13.0, the endpoint is not being resolved correctly, and rather than failing, is ending up with an empty host and port http://:.

Whilst this could be a problem with my test code, it does feel like WithEnvironment() should never be able to resolve with an empty port/host, so maybe something else is wrong.

Expected Behavior

The reference should contain the port & host from the allocated endpoint.

But even more so, I wouldn't expect the final environment value to end up with missing host or port - I'd expect some kind of error to be thrown if either of these evaluated to null/empty.

Steps To Reproduce

   [Test]
   public async Task test()
   {
      var builder = DistributedApplicationTestingBuilder.Create();

      var dependency = builder
         .AddResource(new FakeResource())
         .WithHttpEndpoint();

      var consumer = builder
         .AddContainer("test", "redis")
         .WithImageRegistry("docker.io")
         .WithReference(dependency.GetEndpoint("http"));

      var endpointAnnotation = dependency.Resource.Annotations.OfType<EndpointAnnotation>().Single();
      endpointAnnotation.AllocatedEndpoint = new AllocatedEndpoint(endpointAnnotation, "localhost", 1234);

      using var app = builder.Build();
      await app.StartAsync();

      var envVars = await consumer.Resource.GetEnvironmentVariableValuesAsync();
      Assert.That(envVars["services__fake__http__0"], Is.EqualTo("http://localhost:1234"));
      // Actual: `http://:`
   }

   public class FakeResource() : Resource("fake"), IResourceWithEndpoints { }

Exceptions (if any)

  Assert.That(envVars["services__fake__http__0"], Is.EqualTo("http://localhost:1234"))
  Expected string length 21 but was 8. Strings differ at index 7.
  Expected: "http://localhost:1234"
  But was:  "http://:"

.NET Version info

No response

Anything else?

No response

afscrome avatar Nov 07 '25 21:11 afscrome

For context, here is my "real" test, as opposed to the strip down minimal reproduction above. This is for testing a component that scrapes prometheus metric endpoints, to forward them to the aspire dashboard's otel collector.

   [Test]
   public async Task PullsMetrics(CancellationToken ct)
   {
      var builder = Common.CreateIntegrationAppHost();
      builder.TryAddPrometheusScraper();

      var metricsCalled = new TaskCompletionSource();

      var fakeResoruce = AddResourceWithMetricsEndpoint(builder, () => metricsCalled.TrySetResult());

      var app = builder.Build();
      await app.StartAsync(ct);

      var envVars = (IResourceWithEnvironment)builder.Resources[0];

      await app.ResourceNotifications.WaitForResourceHealthyAsync(PrometheusExtensions.CollectorResourceName, ct);
      await metricsCalled.Task.WaitAsync(ct);


      static IResourceBuilder<MetricsResource> AddResourceWithMetricsEndpoint(IDistributedApplicationBuilder builder, Action callback)
      {
         var port = TestContext.CurrentContext.Random.Next(10000, ushort.MaxValue);

         var resource = new MetricsResource();
         var metrics = builder.AddResource(resource)
            .WithHttpEndpoint(name: "metrics", port: port, isProxied: false);

         var endpoint = metrics.GetEndpoint("metrics");
         metrics.WithPrometheusMetrics(endpoint);

         // As this is a custom resource, aspire won't handle port allcoation for this - fake it.
         var endpointAnnotation = resource.Annotations.OfType<EndpointAnnotation>().Single();
         endpointAnnotation.AllocatedEndpoint = new AllocatedEndpoint(endpointAnnotation, "localhost", port);

         return metrics.OnInitializeResource((resource, _, ct) =>
         {
            var server = new KestrelMetricServer(port);
            server.Start();
            Metrics.DefaultRegistry.AddBeforeCollectCallback(() => Console.WriteLine("Metrics Requested"));
            Metrics.DefaultRegistry.AddBeforeCollectCallback(callback);
            resource.Server = server;

            ct.Register(() => server.Stop());
            return Task.CompletedTask;
         });
      }
   }

afscrome avatar Nov 07 '25 21:11 afscrome

Looks related to @karolz-ms 's changes to endpoints.

davidfowl avatar Nov 08 '25 06:11 davidfowl

I've been able to get my local test passing by moving from using the endpointAnnotation.AllocatedEndpoint setter to the following:

         var endpointAnnotation = resource.Annotations.OfType<EndpointAnnotation>().Single();
         var allocatedEndpoint = new AllocatedEndpoint(endpointAnnotation, KnownHostNames.DockerDesktopHostBridge, port, EndpointBindingMode.SingleAddress, targetPortExpression: port.ToString(), networkID: KnownNetworkIdentifiers.DefaultAspireContainerNetwork);
         var snapshot = new ValueSnapshot<AllocatedEndpoint>();
         snapshot.SetValue(allocatedEndpoint);
         endpointAnnotation.AllAllocatedEndpoints.TryAdd(KnownNetworkIdentifiers.DefaultAspireContainerNetwork, snapshot);

I would however expect using the "old" method to either work, or fail with a clear error.

afscrome avatar Nov 08 '25 18:11 afscrome

Yes, in Aspire 13 AllocatedEndpoints are tied to a network (via NetworkIdentifier) and so if they are used in tests, care needs to be taken to create one with proper network association.

That said @afscrome has a point--on a cursory glance I would expect the call to GetEnvironmentVariableValuesAsync() to "hang" and not resolve to empty values. I will take a look

karolz-ms avatar Nov 10 '25 02:11 karolz-ms