Skip to content
Open
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
Original file line number Diff line number Diff line change
Expand Up @@ -16,9 +16,7 @@

package com.linecorp.armeria.server;

import static com.google.common.base.MoreObjects.firstNonNull;
import static com.google.common.collect.ImmutableList.toImmutableList;
import static com.google.common.collect.ImmutableMap.toImmutableMap;
import static java.util.Objects.requireNonNull;

import java.net.InetAddress;
Expand Down Expand Up @@ -81,7 +79,7 @@ final class DefaultServerConfig implements ServerConfig {
private final VirtualHost defaultVirtualHost;
private final List<VirtualHost> virtualHosts;
@Nullable
private final Int2ObjectMap<Mapping<String, VirtualHost>> virtualHostAndPortMapping;
private volatile Int2ObjectMap<Mapping<String, VirtualHost>> virtualHostAndPortMapping;
private final List<ServiceConfig> services;

private final EventLoopGroup workerGroup;
Expand Down Expand Up @@ -294,26 +292,32 @@ private static Int2ObjectMap<Mapping<String, VirtualHost>> buildDomainAndPortMap
VirtualHost defaultVirtualHost, List<VirtualHost> virtualHosts) {

final List<VirtualHost> portMappingVhosts = virtualHosts.stream()
.filter(v -> v.port() > 0)
.filter(v -> v.port() > 0 ||
(v.serverPort() != null &&
v.serverPort().actualPort() > 0))
.collect(toImmutableList());
final Map<Integer, VirtualHost> portMappingDefaultVhosts =
portMappingVhosts.stream()
.filter(v -> v.hostnamePattern().startsWith("*:"))
.collect(toImmutableMap(VirtualHost::port, Function.identity()));
Copy link
Contributor

@ikhoon ikhoon Feb 19, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Why was the motivation for this refactor? It seems to cause a regression. A unknown host no longer fall back to the default port-based virtual host. The following test passes in the main branch but fails with this PR.

diff --git core/src/test/java/com/linecorp/armeria/server/PortBasedVirtualHostTest.java core/src/test/java/com/linecorp/armeria/server/PortBasedVirtualHostTest.java
index c759fe6b2..1bd47c390 100644
--- core/src/test/java/com/linecorp/armeria/server/PortBasedVirtualHostTest.java
+++ core/src/test/java/com/linecorp/armeria/server/PortBasedVirtualHostTest.java
@@ -60,13 +60,16 @@ class PortBasedVirtualHostTest {
             sb.http(normalServerPort)
               .http(virtualHostPort)
               .http(fooHostPort)
+              .virtualHost("foo.com:" + fooHostPort)
+              .service("/foo", (ctx, req) -> HttpResponse.of("foo with port"))
+              .and()
+              .virtualHost(fooHostPort)
+              .service("/foo-default", (ctx, req) -> HttpResponse.of("foo with default host"))
+              .and()
               .service("/normal", (ctx, req) -> HttpResponse.of("normal"))
               .virtualHost(virtualHostPort)
               .service("/managed", (ctx, req) -> HttpResponse.of("managed"))
               .and()
-              .virtualHost("foo.com:" + fooHostPort)
-              .service("/foo", (ctx, req) -> HttpResponse.of("foo with port"))
-              .and()
               .virtualHost("foo.com")
               .service("/foo-no-port", (ctx, req) -> HttpResponse.of("foo without port"))
               .and()
@@ -154,6 +157,37 @@ class PortBasedVirtualHostTest {
         }
     }
 
+    @Test
+    void shouldFallbackToDefaultPortVirtualHost() {
+        try (ClientFactory factory = ClientFactory.builder()
+                                                  .addressResolverGroupFactory(
+                                                          unused -> MockAddressResolverGroup.localhost())
+                                                  .build()) {
+
+            final WebClient client = WebClient.builder("http://foo.com:" + fooHostPort)
+                                              .factory(factory)
+                                              .build();
+            AggregatedHttpResponse response = client.get("/normal").aggregate().join();
+            // Fallback to default virtual host
+            assertThat(response.contentUtf8()).isEqualTo("normal");
+
+            response = client.get("/managed").aggregate().join();
+            assertThat(response.status()).isEqualTo(HttpStatus.NOT_FOUND);
+
+            response = client.get("/foo").aggregate().join();
+            assertThat(response.status()).isEqualTo(HttpStatus.OK);
+
+            response = client.get("/foo-no-port").aggregate().join();
+            assertThat(response.status()).isEqualTo(HttpStatus.NOT_FOUND);
+
+            final WebClient barClient = WebClient.builder("http://bar.com:" + fooHostPort)
+                                                 .factory(factory)
+                                                 .build();
+            response = barClient.get("/foo-default").aggregate().join();
+            assertThat(response.contentUtf8()).isEqualTo("foo with default host");
+        }
+    }
+
     @Test
     void zeroVirtualHostPort() {
         assertThatThrownBy(() -> Server.builder().virtualHost(0))


final Map<Integer, DomainMappingBuilder<VirtualHost>> mappingsBuilder = new HashMap<>();
for (VirtualHost virtualHost : portMappingVhosts) {
final int port = virtualHost.port();
final int port;
if (virtualHost.port() > 0) {
port = virtualHost.port();
} else {
final ServerPort serverPort = requireNonNull(virtualHost.serverPort());
port = serverPort.actualPort();
}
// The default virtual host should be either '*' or '*:<port>'.
final VirtualHost defaultVhost =
firstNonNull(portMappingDefaultVhosts.get(port), defaultVirtualHost);
final VirtualHost defaultVhost;
if ("*".equals(virtualHost.originalHostnamePattern())) {
defaultVhost = virtualHost;
} else {
defaultVhost = defaultVirtualHost;
}
// Builds a 'DomainMappingBuilder' with 'defaultVhost' for the port if absent.
final DomainMappingBuilder<VirtualHost> mappingBuilder =
mappingsBuilder.computeIfAbsent(port, key -> new DomainMappingBuilder<>(defaultVhost));

if (defaultVhost == virtualHost) {
// The 'virtualHost' was added already as a default value when creating 'DomainMappingBuilder'.
} else {
if (defaultVhost != virtualHost) {
mappingBuilder.add(virtualHost.hostnamePattern(), virtualHost);
}
}
Expand All @@ -332,7 +336,11 @@ private static Mapping<String, VirtualHost> buildDomainMapping(VirtualHost defau
// Set virtual host definitions and initialize their domain name mapping.
final DomainMappingBuilder<VirtualHost> mappingBuilder = new DomainMappingBuilder<>(defaultVirtualHost);
for (VirtualHost h : virtualHosts) {
if (h.port() > 0) {
if (h == defaultVirtualHost) {
// The default virtual host is already set as the default of the DomainMappingBuilder.
continue;
}
if (h.port() > 0 || h.serverPort() != null) {
// A port-based virtual host will be handled by buildDomainAndPortMapping().
continue;
}
Expand Down Expand Up @@ -398,6 +406,19 @@ void setServer(Server server) {
this.server = requireNonNull(server, "server");
}

/**
* Rebuilds the domain and port mapping after all ports are bound.
* This includes ServerPort-based VirtualHosts whose actual ports are now resolved.
*
* <p>Note: {@code this.virtualHosts} includes the {@code defaultVirtualHost},
* which is filtered out inside {@link #buildDomainMapping}.
*/
void rebuildDomainAndPortMapping() {
if (virtualHosts.stream().anyMatch(v -> v.serverPort() != null)) {
virtualHostAndPortMapping = buildDomainAndPortMapping(defaultVirtualHost, virtualHosts);
}
}

@Override
public List<ServerPort> ports() {
return ports;
Expand Down Expand Up @@ -437,9 +458,9 @@ public VirtualHost findVirtualHost(String hostname, int port) {
return virtualHost;
}
}

// No port-based virtual host is configured. Look for named-based virtual host.
final Mapping<String, VirtualHost> nameBasedMapping = virtualHostAndPortMapping.get(-1);
assert nameBasedMapping != null;
final Mapping<String, VirtualHost> nameBasedMapping = requireNonNull(virtualHostAndPortMapping.get(-1));
return nameBasedMapping.map(hostname);
}

Expand Down
12 changes: 10 additions & 2 deletions core/src/main/java/com/linecorp/armeria/server/Server.java
Original file line number Diff line number Diff line change
Expand Up @@ -487,8 +487,11 @@ protected CompletionStage<Void> doStart(@Nullable Void arg) {
try {
doStart(primary).addListener(new ServerPortStartListener(primary))
.addListener(new NextServerPortStartListener(this, it, future));
// Chain the future to set up server metrics first before server start future is completed.
return future.thenAccept(unused -> setupPendingResponsesMetrics());
// Chain the future to set up server metrics and port mapping before server start future is completed.
return future.thenAccept(unused -> {
config.delegate().rebuildDomainAndPortMapping();
setupPendingResponsesMetrics();
});
} catch (Throwable cause) {
future.completeExceptionally(cause);
return future;
Expand Down Expand Up @@ -814,6 +817,11 @@ public void operationComplete(ChannelFuture f) {
assert serverPortMetric != null;
actualPort.setServerPortMetric(serverPortMetric);

// Set the actual port on the original ServerPort for ephemeral ports
if (port.localAddress().getPort() == 0) {
port.setActualPort(actualPort.localAddress().getPort());
}

// Update the boss thread so its name contains the actual port.
Thread.currentThread().setName(bossThreadName(actualPort));

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -1857,6 +1857,52 @@ public VirtualHostBuilder virtualHost(int port) {
return virtualHostBuilder;
}

/**
* Adds the <a href="https://en.wikipedia.org/wiki/Virtual_hosting#Port-based">port-based virtual host</a>
* with the specified {@link ServerPort}. This method can be used to bind a virtual host to a
* random port (port 0).
*
* <p>Note that you cannot configure TLS to the port-based virtual host. Configure it to the
* {@link ServerBuilder} or a {@linkplain #virtualHost(String) name-based virtual host}.
*
* <p>Example usage for random ports:
* <pre>{@code
* ServerPort port1 = new ServerPort(0, SessionProtocol.HTTP);
* ServerPort port2 = new ServerPort(0, SessionProtocol.HTTP);
*
* Server server = Server.builder()
* .port(port1)
* .virtualHost(port1)
* .service("/foo", (ctx, req) -> HttpResponse.of("foo"))
* .and()
* .port(port2)
* .virtualHost(port2)
* .service("/bar", (ctx, req) -> HttpResponse.of("bar"))
* .and()
* .build();
* }</pre>
*
* @param serverPort the {@link ServerPort} that this virtual host binds to
* @return {@link VirtualHostBuilder} for building the virtual host
*/
@UnstableApi
public VirtualHostBuilder virtualHost(ServerPort serverPort) {
requireNonNull(serverPort, "serverPort");

// Look for a virtual host that has already been made with the same ServerPort instance.
final Optional<VirtualHostBuilder> vhost =
virtualHostBuilders.stream()
.filter(v -> v.serverPort() == serverPort && v.defaultVirtualHost())
.findFirst();
if (vhost.isPresent()) {
return vhost.get();
}

final VirtualHostBuilder virtualHostBuilder = new VirtualHostBuilder(this, serverPort);
virtualHostBuilders.add(virtualHostBuilder);
return virtualHostBuilder;
}

private VirtualHostBuilder findOrCreateVirtualHostBuilder(String hostnamePattern) {
requireNonNull(hostnamePattern, "hostnamePattern");
final HostAndPort hostAndPort = HostAndPort.fromString(hostnamePattern);
Expand Down Expand Up @@ -2458,6 +2504,16 @@ DefaultServerConfig buildServerConfig(List<ServerPort> serverPorts) {
virtualHostPort, portNumbers);
}

for (VirtualHostBuilder vhb : virtualHostBuilders) {
final ServerPort serverPort = vhb.serverPort();
if (serverPort != null) {
final boolean presentByIdentity = this.ports.stream().anyMatch(p -> p == serverPort);
checkState(presentByIdentity,
"The ServerPort for a virtual host is not in the server's port list. " +
"Please add the ServerPort using port(ServerPort) before creating a virtual host.");
}
}

checkState(defaultSslContext == null || tlsProvider == null,
"Can't set %s with a static TLS setting", TlsProvider.class.getSimpleName());
if (defaultSslContext == null && tlsProvider == null) {
Expand Down
38 changes: 38 additions & 0 deletions core/src/main/java/com/linecorp/armeria/server/ServerPort.java
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@
package com.linecorp.armeria.server;

import static com.google.common.base.Preconditions.checkArgument;
import static com.google.common.base.Preconditions.checkState;
import static com.linecorp.armeria.common.SessionProtocol.HTTP;
import static com.linecorp.armeria.common.SessionProtocol.HTTPS;
import static com.linecorp.armeria.common.SessionProtocol.PROXY;
Expand Down Expand Up @@ -69,6 +70,11 @@ static long nextPortGroup() {
@Nullable
private ServerPortMetric serverPortMetric;

/**
* The actual port after binding. -1 means not set yet.
*/
private volatile int actualPort = -1;

@Nullable
private String strVal;

Expand Down Expand Up @@ -228,6 +234,38 @@ long portGroup() {
return portGroup;
}

/**
* Returns the actual port this {@link ServerPort} is bound to.
* If the port was configured as 0 (ephemeral) and has been bound, returns the actual bound port.
* Otherwise, returns the configured port from {@link #localAddress()}.
*/
@UnstableApi
public int actualPort() {
final int actualPort = this.actualPort;
if (actualPort > 0) {
return actualPort;
}
return localAddress.getPort();
}

/**
* Sets the actual port after binding.
* This is only allowed when the configured port is 0 (ephemeral) and hasn't been set yet.
*
* @param actualPort the actual bound port
* @throws IllegalArgumentException if actualPort is not positive
* @throws IllegalStateException if the configured port is not 0 or actualPort was already set
*/
void setActualPort(int actualPort) {
checkArgument(actualPort > 0, "actualPort: %s (expected: > 0)", actualPort);
checkState(localAddress.getPort() == 0,
"Cannot set actualPort for non-ephemeral port: %s", localAddress.getPort());
checkState(this.actualPort == -1,
"actualPort is already set to %s", this.actualPort);
this.actualPort = actualPort;
}


@Nullable
ServerPortMetric serverPortMetric() {
return serverPortMetric;
Expand Down
49 changes: 46 additions & 3 deletions core/src/main/java/com/linecorp/armeria/server/VirtualHost.java
Original file line number Diff line number Diff line change
Expand Up @@ -84,6 +84,12 @@ public final class VirtualHost {
private final String hostnamePattern;
private final int port;
@Nullable
private final ServerPort serverPort;
@Nullable
private volatile String cachedDefaultHostname;
@Nullable
private volatile String cachedHostnamePattern;
@Nullable
private final SslContext sslContext;
@Nullable
private final TlsProvider tlsProvider;
Expand Down Expand Up @@ -112,6 +118,7 @@ public final class VirtualHost {
private final Function<RoutingContext, RequestId> requestIdGenerator;

VirtualHost(String defaultHostname, String hostnamePattern, int port,
@Nullable ServerPort serverPort,
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This change seems to introduce a compilation error in RoutersBenchmark.

@Nullable SslContext sslContext,
@Nullable TlsProvider tlsProvider,
@Nullable TlsEngineType tlsEngineType,
Expand Down Expand Up @@ -142,6 +149,7 @@ public final class VirtualHost {
this.hostnamePattern = hostnamePattern;
}
this.port = port;
this.serverPort = serverPort;
this.sslContext = sslContext;
this.tlsProvider = tlsProvider;
this.tlsEngineType = tlsEngineType;
Expand Down Expand Up @@ -182,7 +190,8 @@ VirtualHost withNewSslContext(SslContext sslContext) {
ReferenceCountUtil.release(sslContext);
throw new IllegalStateException("Cannot set a new SslContext when TlsProvider is set.");
}
return new VirtualHost(originalDefaultHostname, originalHostnamePattern, port, sslContext, null,
return new VirtualHost(originalDefaultHostname, originalHostnamePattern, port, serverPort,
sslContext, null,
tlsEngineType, serviceConfigs, fallbackServiceConfig,
RejectedRouteHandler.DISABLED, host -> accessLogger, defaultServiceNaming,
defaultLogName, requestTimeoutMillis, maxRequestLength, verboseResponses,
Expand Down Expand Up @@ -309,9 +318,21 @@ void setServerConfig(ServerConfig serverConfig) {
}

/**
* Returns the default hostname of this virtual host.
* Returns the name of the default host.
*/
public String defaultHostname() {
if (serverPort != null) {
String cached = cachedDefaultHostname;
if (cached != null) {
return cached;
}
final int actualPort = serverPort.actualPort();
if (actualPort > 0) {
cached = originalDefaultHostname + ':' + actualPort;
cachedDefaultHostname = cached;
return cached;
}
}
return defaultHostname;
}

Expand All @@ -320,6 +341,18 @@ public String defaultHostname() {
* <a href="https://datatracker.ietf.org/doc/html/rfc2818#section-3.1">the section 3.1 of RFC2818</a>.
*/
public String hostnamePattern() {
if (serverPort != null) {
String cached = cachedHostnamePattern;
if (cached != null) {
return cached;
}
final int actualPort = serverPort.actualPort();
if (actualPort > 0) {
cached = originalHostnamePattern + ':' + actualPort;
cachedHostnamePattern = cached;
return cached;
}
}
return hostnamePattern;
}

Expand All @@ -331,6 +364,15 @@ public int port() {
return port;
}

/**
* Returns the {@link ServerPort} that this virtual host is bound to,
* or {@code null} if this virtual host is not based on a {@link ServerPort}.
*/
@Nullable
ServerPort serverPort() {
return serverPort;
}

/**
* Returns the {@link SslContext} of this virtual host.
*/
Expand Down Expand Up @@ -602,7 +644,8 @@ VirtualHost decorate(@Nullable Function<? super HttpService, ? extends HttpServi
final ServiceConfig fallbackServiceConfig =
this.fallbackServiceConfig.withDecoratedService(decorator);

return new VirtualHost(originalDefaultHostname, originalHostnamePattern, port, sslContext, tlsProvider,
return new VirtualHost(originalDefaultHostname, originalHostnamePattern, port, serverPort,
sslContext, tlsProvider,
tlsEngineType, serviceConfigs, fallbackServiceConfig,
RejectedRouteHandler.DISABLED, host -> accessLogger, defaultServiceNaming,
defaultLogName, requestTimeoutMillis, maxRequestLength, verboseResponses,
Expand Down
Loading
Loading