Skip to content

Add VirtualHost support for random ports (#6410)#6603

Open
junjunclub wants to merge 6 commits intoline:mainfrom
junjunclub:feature/virtualhost-random-port-support
Open

Add VirtualHost support for random ports (#6410)#6603
junjunclub wants to merge 6 commits intoline:mainfrom
junjunclub:feature/virtualhost-random-port-support

Conversation

@junjunclub
Copy link

@junjunclub junjunclub commented Jan 24, 2026

Motivation

Currently, using virtualHost(0) throws an IllegalArgumentException:
java.lang.IllegalArgumentException: port: 0 (expected: 1-65535)

This is problematic for CI environments where random ports are essential to avoid port conflicts. The existing virtualHost(int port) API cannot support random ports because:

  • The int value 0 cannot distinguish between multiple random ports.
  • Calling virtualHost(0) twice returns the same VirtualHostBuilder due to the builder reuse logic.

See: issue #6410

Modifications

  • Added ServerBuilder.virtualHost(ServerPort) API that uses reference equality to distinguish different random port configurations.
  • Added VirtualHostBuilder.serverPort field and constructor to support ServerPort-based virtual hosts.
  • Added VirtualHost.serverPort field and accessor.
  • Added ServerPort.originalServerPort field to track the original configuration when binding to ephemeral ports.
  • Passed original ServerPort reference when creating actualPort in Server.java.
  • Updated DefaultServerConfig.findVirtualHost() to route requests based on the actual bound port (priority lookup).
  • Added unit tests for VirtualHostBuilder.serverPort(), VirtualHost.serverPort(), and ServerPort.originalServerPort() accessors.
  • Added integration test for the new virtualHost(ServerPort) API routing behavior.

Result

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();

Each ServerPort instance is assigned a different random port by the OS, and requests are correctly routed to the corresponding VirtualHost.


Follow-up: O(1) ServerPort-based VirtualHost lookup (new in this PR)

While implementing virtualHost(ServerPort), we noticed that findVirtualHost(String hostname, int port) can become inefficient for ServerPort-based virtual hosts using ephemeral ports, because it may scan activePorts and virtualHosts to resolve the actual bound port.

This PR also introduces an actualPort() approach and a precomputed mapping to make this routing step O(1) after all ports are bound.

Additional changes

  • ServerPort.actualPort()

    • Added volatile int actualPort to store the actual bound port.
    • Added actualPort() getter and package-private setActualPort(int) setter.
  • Server.java

    • Call port.setActualPort(...) after binding ephemeral ports.
  • DefaultServerConfig

    • Added volatile Map<Integer, VirtualHost> actualPortToVirtualHost.
    • Added buildActualPortMapping() to populate the mapping after all ports are bound.
    • Updated findVirtualHost(String, int) to prefer the O(1) map lookup for ServerPort-based routing.
  • Tests

    • Added unit tests for ServerPort.actualPort() getter/setter behavior.
    • Added integration tests verifying actualPort is reflected after server start and that multiple random-port virtual hosts work as expected.

Outcome

  • findVirtualHost() now performs O(1) lookup for ServerPort-based virtual host routing once ports are bound.
  • ServerPort.actualPort() provides a clean API to retrieve the actual bound port.

@coderabbitai
Copy link

coderabbitai bot commented Jan 24, 2026

Note

Reviews paused

It looks like this branch is under active development. To avoid overwhelming you with review comments due to an influx of new commits, CodeRabbit has automatically paused this review. You can configure this behavior by changing the reviews.auto_review.auto_pause_after_reviewed_commits setting.

Use the following commands to manage reviews:

  • @coderabbitai resume to resume automatic reviews.
  • @coderabbitai review to trigger a single review.

Use the checkboxes below for quick actions:

  • ▶️ Resume reviews
  • 🔍 Trigger review
📝 Walkthrough

Walkthrough

Adds ServerPort-aware virtual hosts and runtime port mapping: ServerPort now tracks actual bound ports; ServerBuilder gains virtualHost(ServerPort) and validates ServerPort presence; VirtualHost/VirtualHostBuilder carry ServerPort; DefaultServerConfig builds/rebuilds domain-and-port mappings after ports bind; tests added for ephemeral-port scenarios.

Changes

Cohort / File(s) Summary
Server core: ServerPort & binding
core/src/main/java/com/linecorp/armeria/server/ServerPort.java, core/src/main/java/com/linecorp/armeria/server/Server.java
Add actualPort field, actualPort() accessor and setActualPort(); propagate bound ephemeral port back to the original ServerPort after bind; Server.doStart triggers mapping rebuild.
Builder API & validation
core/src/main/java/com/linecorp/armeria/server/ServerBuilder.java
Add public VirtualHostBuilder virtualHost(ServerPort) overload; validate that any VirtualHost bound to a ServerPort references a ServerPort present in the server's ports list at build time.
VirtualHost wiring
core/src/main/java/com/linecorp/armeria/server/VirtualHost.java, core/src/main/java/com/linecorp/armeria/server/VirtualHostBuilder.java
Carry optional ServerPort through constructors and builders; add accessor; preserve serverPort during decoration/ssl changes; augment cached hostname/hostnamePattern to include serverPort.actualPort() when bound.
Runtime config & resolution
core/src/main/java/com/linecorp/armeria/server/DefaultServerConfig.java
Make per-port mapping volatile; exclude port-bound hosts from name-only domain map; add rebuildDomainAndPortMapping() and adapt findVirtualHost(hostname, port) to consult port mappings (including ServerPort.actualPort()) before name-based lookup.
Tests
core/src/test/java/com/linecorp/armeria/server/PortBasedVirtualHostTest.java, core/src/test/java/com/linecorp/armeria/server/ServerPortTest.java
Add/extend tests for ServerPort behavior and ServerPort-based virtual hosts: ephemeral-port binding, setActualPort validation, virtualHost(ServerPort) semantics, routing per-port, and reuse/isolation cases.

Sequence Diagram(s)

sequenceDiagram
    participant App as Application
    participant SB as ServerBuilder
    participant SP as ServerPort
    participant VHB as VirtualHostBuilder
    participant S as Server
    participant DSC as DefaultServerConfig
    participant Client as Client

    App->>SB: port(ServerPort) / port(0)
    SB->>SP: create ServerPort(ephemeral)
    App->>SB: virtualHost(ServerPort)
    SB->>VHB: new VirtualHostBuilder(ServerPort)
    VHB-->>SB: register VirtualHostBuilder(serverPort)
    App->>SB: build()
    SB->>S: build/start
    S->>SP: bind to actual port (OS)
    S->>SP: setActualPort(actualPort)
    S->>DSC: rebuildDomainAndPortMapping()
    Client->>DSC: findVirtualHost(hostname, actualPort)
    DSC-->>Client: return VirtualHost bound to original ServerPort
Loading

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~45 minutes

Suggested labels

new feature

Suggested reviewers

  • trustin
  • minwoox
  • ikhoon

Poem

🐇 I nudged the zero, held tight to my seed,
Kept my origin through bind and need.
When OS gave a number, I claimed my spot,
Hosts find their door where the true port got. 🥕

🚥 Pre-merge checks | ✅ 4 | ❌ 2

❌ Failed checks (2 warnings)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 29.41% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
Merge Conflict Detection ⚠️ Warning ❌ Merge conflicts detected (145 files):

⚔️ annotation-processor/src/main/java/com/linecorp/armeria/server/annotation/processor/DocumentationProcessor.java (content)
⚔️ annotation-processor/src/test/java/com/linecorp/armeria/server/annotation/processor/DocumentationProcessorTest.java (content)
⚔️ annotation-processor/src/test/resources/testing/DocumentationProcessor/WithJavaDoc.java.txt (content)
⚔️ athenz/src/main/java/com/linecorp/armeria/client/athenz/AthenzClient.java (content)
⚔️ athenz/src/main/java/com/linecorp/armeria/client/athenz/AthenzClientBuilder.java (content)
⚔️ athenz/src/main/java/com/linecorp/armeria/common/athenz/TokenType.java (content)
⚔️ athenz/src/main/java/com/linecorp/armeria/server/athenz/AbstractAthenzServiceBuilder.java (content)
⚔️ athenz/src/main/java/com/linecorp/armeria/server/athenz/AthenzService.java (content)
⚔️ athenz/src/main/java/com/linecorp/armeria/server/athenz/AthenzServiceBuilder.java (content)
⚔️ athenz/src/main/java/com/linecorp/armeria/server/athenz/AthenzServiceDecoratorFactory.java (content)
⚔️ athenz/src/main/java/com/linecorp/armeria/server/athenz/AthenzServiceDecoratorFactoryBuilder.java (content)
⚔️ athenz/src/main/java/com/linecorp/armeria/server/athenz/MinifiedAuthZpeClient.java (content)
⚔️ athenz/src/main/java/com/linecorp/armeria/server/athenz/RequiresAthenzRole.java (content)
⚔️ athenz/src/main/java/com/linecorp/armeria/server/athenz/resource/HeaderAthenzResourceProvider.java (content)
⚔️ athenz/src/main/java/com/linecorp/armeria/server/athenz/resource/JsonBodyFieldAthenzResourceProvider.java (content)
⚔️ athenz/src/main/java/com/linecorp/armeria/server/athenz/resource/PathAthenzResourceProvider.java (content)
⚔️ athenz/src/test/java/com/linecorp/armeria/server/athenz/AthenzAnnotatedServiceTest.java (content)
⚔️ athenz/src/test/java/com/linecorp/armeria/server/athenz/AthenzDocker.java (content)
⚔️ core/src/main/java/com/linecorp/armeria/client/endpoint/PropertiesEndpointGroup.java (content)
⚔️ core/src/main/java/com/linecorp/armeria/common/util/EventLoopGroupBuilder.java (content)
⚔️ core/src/main/java/com/linecorp/armeria/internal/common/stream/DecodedStreamMessage.java (content)
⚔️ core/src/main/java/com/linecorp/armeria/internal/server/annotation/AnnotatedDocServicePlugin.java (content)
⚔️ core/src/main/java/com/linecorp/armeria/internal/server/annotation/AnnotatedServiceFactory.java (content)
⚔️ core/src/main/java/com/linecorp/armeria/internal/server/annotation/AnnotatedValueResolver.java (content)
⚔️ core/src/main/java/com/linecorp/armeria/server/DefaultServerConfig.java (content)
⚔️ core/src/main/java/com/linecorp/armeria/server/HttpHeaderUtil.java (content)
⚔️ core/src/main/java/com/linecorp/armeria/server/Server.java (content)
⚔️ core/src/main/java/com/linecorp/armeria/server/ServerBuilder.java (content)
⚔️ core/src/main/java/com/linecorp/armeria/server/ServerPort.java (content)
⚔️ core/src/main/java/com/linecorp/armeria/server/VirtualHost.java (content)
⚔️ core/src/main/java/com/linecorp/armeria/server/VirtualHostBuilder.java (content)
⚔️ core/src/main/java/com/linecorp/armeria/server/docs/DocServicePlugin.java (content)
⚔️ core/src/main/java/com/linecorp/armeria/server/docs/DocStringSupport.java (content)
⚔️ core/src/main/java/com/linecorp/armeria/server/docs/ExampleSupport.java (content)
⚔️ core/src/main/java/com/linecorp/armeria/server/docs/JsonSchemaGenerator.java (content)
⚔️ core/src/main/java/com/linecorp/armeria/server/docs/MethodInfo.java (content)
⚔️ core/src/main/java/com/linecorp/armeria/server/docs/ServiceInfo.java (content)
⚔️ core/src/main/java/com/linecorp/armeria/server/docs/ServiceSpecification.java (content)
⚔️ core/src/test/java/com/linecorp/armeria/client/ConnectionPoolKeepAliveTest.java (content)
⚔️ core/src/test/java/com/linecorp/armeria/client/endpoint/PropertiesEndpointGroupTest.java (content)
⚔️ core/src/test/java/com/linecorp/armeria/client/retry/ResponseTimeoutFromStartTest.java (content)
⚔️ core/src/test/java/com/linecorp/armeria/common/stream/DecodedStreamMessageTest.java (content)
⚔️ core/src/test/java/com/linecorp/armeria/internal/common/util/IpAddrUtilTest.java (content)
⚔️ core/src/test/java/com/linecorp/armeria/internal/server/annotation/AnnotatedDocServicePluginTest.java (content)
⚔️ core/src/test/java/com/linecorp/armeria/internal/server/annotation/AnnotatedDocServiceTest.java (content)
⚔️ core/src/test/java/com/linecorp/armeria/internal/server/annotation/AnnotatedServiceFactoryTest.java (content)
⚔️ core/src/test/java/com/linecorp/armeria/server/HttpHeaderUtilTest.java (content)
⚔️ core/src/test/java/com/linecorp/armeria/server/PortBasedVirtualHostTest.java (content)
⚔️ core/src/test/java/com/linecorp/armeria/server/ServerPortTest.java (content)
⚔️ core/src/test/java/com/linecorp/armeria/server/docs/JsonSchemaGeneratorTest.java (content)
⚔️ core/src/test/java/com/linecorp/armeria/server/docs/MethodInfoTest.java (content)
⚔️ core/src/test/java/com/linecorp/armeria/server/docs/ServiceInfoTest.java (content)
⚔️ core/src/test/resources/testing/core/DefaultDescriptiveTypeInfoProviderTest/JsonDescriptiveTypeInfoProviderTest_specification.json5 (content)
⚔️ core/src/test/resources/testing/core/DefaultDescriptiveTypeInfoProviderTest/ReflectiveDescriptiveTypeInfoProviderTest_specification.json5 (content)
⚔️ dependencies.toml (content)
⚔️ docs-client/src/components/VariableList/index.tsx (content)
⚔️ docs-client/src/containers/App/index.tsx (content)
⚔️ docs-client/src/containers/MethodPage/Exceptions.tsx (content)
⚔️ docs-client/src/containers/MethodPage/ReturnType.tsx (content)
⚔️ docs-client/src/containers/MethodPage/index.tsx (content)
⚔️ docs-client/src/containers/OverviewPage/index.tsx (content)
⚔️ docs-client/src/lib/specification.tsx (content)
⚔️ gradle.properties (content)
⚔️ graphql/src/main/java/com/linecorp/armeria/internal/server/graphql/GraphqlDocServicePlugin.java (content)
⚔️ graphql/src/test/java/com/linecorp/armeria/internal/server/graphql/GraphqlDocServiceTest.java (content)
⚔️ grpc/src/main/java/com/linecorp/armeria/common/grpc/GrpcMeterIdPrefixFunction.java (content)
⚔️ grpc/src/main/java/com/linecorp/armeria/internal/server/grpc/GrpcDocServicePlugin.java (content)
⚔️ grpc/src/main/java/com/linecorp/armeria/internal/server/grpc/GrpcDocStringExtractor.java (content)
⚔️ grpc/src/test/java/com/linecorp/armeria/common/grpc/GrpcMeterIdPrefixFunctionTest.java (content)
⚔️ grpc/src/test/java/com/linecorp/armeria/internal/server/grpc/GrpcDocServicePluginTest.java (content)
⚔️ grpc/src/test/java/com/linecorp/armeria/internal/server/grpc/GrpcDocServiceTest.java (content)
⚔️ grpc/src/test/java/com/linecorp/armeria/internal/server/grpc/GrpcDocStringExtractorTest.java (content)
⚔️ grpc/src/test/proto/testing/grpc/test.proto (content)
⚔️ it/thrift0.9.1/build.gradle (content)
⚔️ it/thrift0.9.1/src/test/thrift/main.thrift (content)
⚔️ it/xds-client/src/test/java/com/linecorp/armeria/xds/it/BootstrapTest.java (content)
⚔️ it/xds-client/src/test/java/com/linecorp/armeria/xds/it/ErrorHandlingTest.java (content)
⚔️ it/xds-client/src/test/java/com/linecorp/armeria/xds/it/ResourceNodeMetricTest.java (content)
⚔️ it/xds-client/src/test/java/com/linecorp/armeria/xds/it/XdsLoadBalancerLifecycleObserverTest.java (content)
⚔️ protobuf/src/test/resources/testing/protobuf/ProtobufDescriptiveTypeInfoProviderTest_specification.json5 (content)
⚔️ scalapb/scalapb_2.13/src/test/resources/testing/scalapb/ScalaPbDescriptiveTypeInfoProviderTest_specification.json5 (content)
⚔️ settings.gradle (content)
⚔️ site-new/package-lock.json (content)
⚔️ site-new/package.json (content)
⚔️ spring/boot4-actuator-autoconfigure/src/main/java/com/linecorp/armeria/spring/actuate/WebOperationService.java (content)
⚔️ thrift/thrift0.13/src/main/java/com/linecorp/armeria/internal/server/thrift/ThriftDocServicePlugin.java (content)
⚔️ thrift/thrift0.13/src/main/java/com/linecorp/armeria/internal/server/thrift/ThriftDocStringExtractor.java (content)
⚔️ thrift/thrift0.13/src/test/java/com/linecorp/armeria/internal/server/thrift/ThriftDocServicePluginTest.java (content)
⚔️ thrift/thrift0.13/src/test/java/com/linecorp/armeria/internal/server/thrift/ThriftDocServiceTest.java (content)
⚔️ thrift/thrift0.13/src/test/java/com/linecorp/armeria/internal/server/thrift/ThriftDocStringExtractorTest.java (content)
⚔️ thrift/thrift0.13/src/test/resources/META-INF/armeria/thrift/ThriftTest.json (content)
⚔️ thrift/thrift0.13/src/test/resources/META-INF/armeria/thrift/ThriftTest.thrift (content)
⚔️ thrift/thrift0.13/src/test/thrift/main.thrift (content)
⚔️ thrift/thrift0.17/src/test/java/com/linecorp/armeria/internal/server/thrift/ThriftDocServicePluginTest.java (content)
⚔️ xds/src/main/java/com/linecorp/armeria/xds/AbstractRoot.java (content)
⚔️ xds/src/main/java/com/linecorp/armeria/xds/BootstrapClusters.java (content)
⚔️ xds/src/main/java/com/linecorp/armeria/xds/ClusterRoot.java (content)
⚔️ xds/src/main/java/com/linecorp/armeria/xds/ClusterSnapshot.java (content)
⚔️ xds/src/main/java/com/linecorp/armeria/xds/ClusterXdsResource.java (content)
⚔️ xds/src/main/java/com/linecorp/armeria/xds/ConfigSourceMapper.java (content)
⚔️ xds/src/main/java/com/linecorp/armeria/xds/DefaultResponseHandler.java (content)
⚔️ xds/src/main/java/com/linecorp/armeria/xds/DefaultXdsLoadBalancerLifecycleObserver.java (content)
⚔️ xds/src/main/java/com/linecorp/armeria/xds/EndpointXdsResource.java (content)
⚔️ xds/src/main/java/com/linecorp/armeria/xds/GrpcServicesPreprocessor.java (content)
⚔️ xds/src/main/java/com/linecorp/armeria/xds/ListenerManager.java (content)
⚔️ xds/src/main/java/com/linecorp/armeria/xds/ListenerRoot.java (content)
⚔️ xds/src/main/java/com/linecorp/armeria/xds/ListenerSnapshot.java (content)
⚔️ xds/src/main/java/com/linecorp/armeria/xds/ListenerXdsResource.java (content)
⚔️ xds/src/main/java/com/linecorp/armeria/xds/ResourceNode.java (content)
⚔️ xds/src/main/java/com/linecorp/armeria/xds/ResourceNodeMeterBinder.java (content)
⚔️ xds/src/main/java/com/linecorp/armeria/xds/ResourceWatcher.java (content)
⚔️ xds/src/main/java/com/linecorp/armeria/xds/RouteSnapshot.java (content)
⚔️ xds/src/main/java/com/linecorp/armeria/xds/RouteXdsResource.java (content)
⚔️ xds/src/main/java/com/linecorp/armeria/xds/Snapshot.java (content)
⚔️ xds/src/main/java/com/linecorp/armeria/xds/SnapshotWatcher.java (content)
⚔️ xds/src/main/java/com/linecorp/armeria/xds/VirtualHostSnapshot.java (content)
⚔️ xds/src/main/java/com/linecorp/armeria/xds/XdsBootstrapBuilder.java (content)
⚔️ xds/src/main/java/com/linecorp/armeria/xds/XdsBootstrapImpl.java (content)
⚔️ xds/src/main/java/com/linecorp/armeria/xds/XdsClusterManager.java (content)
⚔️ xds/src/main/java/com/linecorp/armeria/xds/XdsStreamSubscriber.java (content)
⚔️ xds/src/main/java/com/linecorp/armeria/xds/client/endpoint/DefaultHostSet.java (content)
⚔️ xds/src/main/java/com/linecorp/armeria/xds/client/endpoint/DefaultLbStateFactory.java (content)
⚔️ xds/src/main/java/com/linecorp/armeria/xds/client/endpoint/DefaultLoadBalancer.java (content)
⚔️ xds/src/main/java/com/linecorp/armeria/xds/client/endpoint/EndpointUtil.java (content)
⚔️ xds/src/main/java/com/linecorp/armeria/xds/client/endpoint/LoadBalancerState.java (content)
⚔️ xds/src/main/java/com/linecorp/armeria/xds/client/endpoint/LocalityRoutingStateFactory.java (content)
⚔️ xds/src/main/java/com/linecorp/armeria/xds/client/endpoint/PrioritySet.java (content)
⚔️ xds/src/main/java/com/linecorp/armeria/xds/client/endpoint/PriorityState.java (content)
⚔️ xds/src/main/java/com/linecorp/armeria/xds/client/endpoint/PriorityStateManager.java (content)
⚔️ xds/src/main/java/com/linecorp/armeria/xds/client/endpoint/RouteConfigSelector.java (content)
⚔️ xds/src/main/java/com/linecorp/armeria/xds/client/endpoint/RouterFilter.java (content)
⚔️ xds/src/main/java/com/linecorp/armeria/xds/client/endpoint/SubsetLoadBalancer.java (content)
⚔️ xds/src/main/java/com/linecorp/armeria/xds/client/endpoint/XdsEndpointGroup.java (content)
⚔️ xds/src/main/java/com/linecorp/armeria/xds/client/endpoint/XdsEndpointUtil.java (content)
⚔️ xds/src/main/java/com/linecorp/armeria/xds/client/endpoint/XdsHttpPreprocessor.java (content)
⚔️ xds/src/main/java/com/linecorp/armeria/xds/client/endpoint/XdsLoadBalancer.java (content)
⚔️ xds/src/main/java/com/linecorp/armeria/xds/client/endpoint/XdsLoadBalancerLifecycleObserver.java (content)
⚔️ xds/src/main/java/com/linecorp/armeria/xds/client/endpoint/XdsPreprocessor.java (content)
⚔️ xds/src/main/java/com/linecorp/armeria/xds/client/endpoint/XdsRpcPreprocessor.java (content)
⚔️ xds/src/test/java/com/linecorp/armeria/xds/AggregatingNodeTest.java (content)
⚔️ xds/src/test/java/com/linecorp/armeria/xds/TestResourceWatcher.java (content)
⚔️ xds/src/test/java/com/linecorp/armeria/xds/client/endpoint/DynamicHealthCheckTest.java (content)
⚔️ xds/src/test/java/com/linecorp/armeria/xds/client/endpoint/HealthCheckedTest.java (content)
⚔️ xds/src/test/java/com/linecorp/armeria/xds/client/endpoint/RampingUpTest.java (content)
⚔️ xds/src/test/java/com/linecorp/armeria/xds/client/endpoint/ZoneAwareTest.java (content)

These conflicts must be resolved before merging into main.
Resolve conflicts locally and push changes to this branch.
✅ Passed checks (4 passed)
Check name Status Explanation
Title check ✅ Passed The PR title accurately summarizes the main change: adding VirtualHost support for random ports, which is the core objective.
Description check ✅ Passed The PR description is comprehensive and directly related to the changeset, explaining motivation, modifications, and results with code examples.
Linked Issues check ✅ Passed The PR fully addresses issue #6410 by implementing ServerBuilder.virtualHost(ServerPort) API, ServerPort.actualPort() tracking, O(1) lookup via actualPort mapping, and comprehensive tests.
Out of Scope Changes check ✅ Passed All changes are within scope: ServerPort-based VirtualHost support, actualPort tracking/mapping, and related infrastructure in DefaultServerConfig, Server, VirtualHost, and test coverage.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing touches
  • 📝 Generate docstrings
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Post copyable unit tests in a comment
⚔️ Resolve merge conflicts (beta)
  • Auto-commit resolved conflicts to branch feature/virtualhost-random-port-support
  • Post resolved changes as copyable diffs in a comment

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

Copy link

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 2

🤖 Fix all issues with AI agents
In `@core/src/main/java/com/linecorp/armeria/server/ServerBuilder.java`:
- Around line 2506-2513: The validation currently uses
this.ports.contains(serverPort) which relies on equals() but runtime matching
requires reference identity; update the check in the ServerBuilder validation
loop (involving VirtualHostBuilder and its serverPort() call) to test identity
instead of equality (e.g., ensure any entry in this.ports is == serverPort) and
keep the existing checkState message; locate the block using VirtualHostBuilder
vhb, serverPort(), and this.ports and replace the contains(...) condition with
an identity-based predicate (anyMatch(p -> p == serverPort) or equivalent).
- Around line 1860-1889: The new public overload virtualHost(ServerPort) in
ServerBuilder must be annotated with `@UnstableApi` to mark the API as unstable;
update the declaration of the virtualHost(ServerPort) method in ServerBuilder to
add the `@UnstableApi` annotation (importing it if needed) so the public API
surface for VirtualHostBuilder returned by virtualHost(ServerPort) is properly
marked per project guidelines.
🧹 Nitpick comments (2)
core/src/test/java/com/linecorp/armeria/server/PortBasedVirtualHostTest.java (1)

241-331: Add assertions for /bar routing in virtualHostWithServerPort.
Right now /bar responses are computed but never asserted, so misrouting could slip through.

✅ Proposed test assertion
             // Check /bar on both ports
             final AggregatedHttpResponse resp1Bar = client1.get("/bar").aggregate().join();
             final AggregatedHttpResponse resp2Bar = client2.get("/bar").aggregate().join();

             // One port should serve /foo with OK, the other with NOT_FOUND
             final boolean port1ServesFoo = resp1Foo.status() == HttpStatus.OK;
             final boolean port2ServesFoo = resp2Foo.status() == HttpStatus.OK;
             assertThat(port1ServesFoo || port2ServesFoo)
                     .as("At least one port should serve /foo, but got: port1=%s, port2=%s",
                         resp1Foo.status(), resp2Foo.status())
                     .isTrue();
+            // One port should serve /bar with OK as well.
+            final boolean port1ServesBar = resp1Bar.status() == HttpStatus.OK;
+            final boolean port2ServesBar = resp2Bar.status() == HttpStatus.OK;
+            assertThat(port1ServesBar || port2ServesBar)
+                    .as("At least one port should serve /bar, but got: port1=%s, port2=%s",
+                        resp1Bar.status(), resp2Bar.status())
+                    .isTrue();
core/src/main/java/com/linecorp/armeria/server/VirtualHost.java (1)

316-341: Consider documenting the dynamic port resolution behavior.

The defaultHostname() and hostnamePattern() methods now return different values depending on whether the server has bound to its ports. Before binding, serverPort.localAddress() returns null, so the pre-computed value is returned. After binding, the actual ephemeral port is appended.

This dynamic behavior is intentional for ephemeral port support, but callers may not expect the return value to change over the VirtualHost lifecycle. Consider adding Javadoc clarification, e.g.:

/**
 * Returns the default hostname of this virtual host. If this virtual host is bound to a
 * {`@link` ServerPort} with an ephemeral port (0), the actual bound port is appended after
 * the server starts.
 */

return buf.toString();
}
}
} No newline at end of file

This comment was marked as resolved.

Copy link
Author

Choose a reason for hiding this comment

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

Thanks! I’ve updated it!

Copy link

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 1

🤖 Fix all issues with AI agents
In
`@core/src/test/java/com/linecorp/armeria/server/PortBasedVirtualHostTest.java`:
- Around line 241-286: The test virtualHostWithServerPort collects responses for
/foo and /bar but never asserts the /bar results; update the test to compute
booleans for resp1Bar.status() and resp2Bar.status() (e.g., port1ServesBar,
port2ServesBar) and add assertions: (1) at least one port serves /bar
(port1ServesBar || port2ServesBar) and (2) ensure exclusivity per port by
asserting each port serves exactly one of the endpoints (for each port assert
(servesFoo ^ servesBar) is true), referencing the existing
resp1Foo/resp2Foo/resp1Bar/resp2Bar variables in virtualHostWithServerPort so
the test verifies both routing and exclusivity.
♻️ Duplicate comments (2)
core/src/main/java/com/linecorp/armeria/server/ServerBuilder.java (2)

1860-1903: Add @UnstableApi to the new public overload.
New public APIs should be annotated as unstable per project guidelines.

✅ Proposed fix
+    `@UnstableApi`
     public VirtualHostBuilder virtualHost(ServerPort serverPort) {
         requireNonNull(serverPort, "serverPort");
As per coding guidelines, ...

2506-2513: Validate ServerPort by identity, not equals.
contains() uses equals, but runtime matching uses reference identity (==), so a different-but-equal instance would pass validation yet never match.

🐛 Proposed fix
-            if (serverPort != null) {
-                checkState(this.ports.contains(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.");
             }
🧹 Nitpick comments (1)
core/src/main/java/com/linecorp/armeria/server/DefaultServerConfig.java (1)

427-445: Consider avoiding per-request full scans when no ServerPort-based vhosts exist.
findVirtualHost(...) now iterates active ports (and then virtual hosts) on every request; a boolean guard or a precomputed map could keep the hot path cheaper.

@codecov
Copy link

codecov bot commented Jan 26, 2026

Codecov Report

❌ Patch coverage is 79.51807% with 17 lines in your changes missing coverage. Please review.
✅ Project coverage is 74.42%. Comparing base (8150425) to head (3004ed6).
⚠️ Report is 347 commits behind head on main.

Files with missing lines Patch % Lines
.../java/com/linecorp/armeria/server/VirtualHost.java 27.27% 13 Missing and 3 partials ⚠️
...ava/com/linecorp/armeria/server/ServerBuilder.java 93.75% 0 Missing and 1 partial ⚠️
Additional details and impacted files
@@             Coverage Diff              @@
##               main    #6603      +/-   ##
============================================
- Coverage     74.46%   74.42%   -0.05%     
- Complexity    22234    23922    +1688     
============================================
  Files          1963     2146     +183     
  Lines         82437    89199    +6762     
  Branches      10764    11688     +924     
============================================
+ Hits          61385    66384    +4999     
- Misses        15918    17243    +1325     
- Partials       5134     5572     +438     

☔ View full report in Codecov by Sentry.
📢 Have feedback on the report? Share it here.

🚀 New features to boost your workflow:
  • ❄️ Test Analytics: Detect flaky tests, report on failures, and find test suite problems.
  • 📦 JS Bundle Analysis: Save yourself from yourself by tracking and limiting bundle sizes in JS merges.

final ServerPort originalPort = activePort.originalServerPort();
if (originalPort != null) {
// Find the VirtualHost that has this original ServerPort
for (VirtualHost vh : virtualHosts) {
Copy link
Contributor

Choose a reason for hiding this comment

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

Question) Is it possible to optimize this?

Personally, I think it is fine if a map of (ephemeral ports -> virtual hosts) is created right after all ports are bound (or each port is bound), and right before ServerStartStopSupport's future is completed.

At the very least, given that it is fairly common for multiple ports/virtual hosts to be specified, I prefer the common path not to iterate through all hosts/ports. This is especially since this is a critical path called for every request.

Copy link
Author

Choose a reason for hiding this comment

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

You're right. My initial focus was on making the feature work correctly with minimal changes,
so I used a simple iteration approach. However, I agree that O(n * m) iteration on every request
is inefficient for this critical path.

I’m planning to use a volatile field with an immutable map for this, as it seems suitable for a write-once, read-many scenario. Would you consider this a sound approach, or do you see any potential pitfalls I might have overlooked?

Copy link
Contributor

Choose a reason for hiding this comment

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

I’m planning to use a volatile field with an immutable map for this, as it seems suitable for a write-once, read-many scenario. Would you consider this a sound approach, or do you see any potential pitfalls I might have overlooked?

Sure, the approach sounds reasonable to me

Copy link
Contributor

@ikhoon ikhoon Jan 27, 2026

Choose a reason for hiding this comment

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

Using random ports for port-based virtual hosting seems more complex than I initially thought. It may not be a good-first-issue so I suggest the following approach as guidance.

 private final Map<ServerPort, Mapping<String, VirtualHost>> virtualHostAndPortMapping; 

Since I haven't implemented a POC, this proposal may not work as intended or could have unexpected issues.

Copy link
Contributor

@jrhee17 jrhee17 Jan 27, 2026

Choose a reason for hiding this comment

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

Store the ServerPort set via ServerBuilder in ServerChannel attributes

If this approach is taken, we should also consider server reconfiguration. (or at least decide whether this will be supported for server reconfiguration)

Copy link
Contributor

Choose a reason for hiding this comment

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

If this approach is taken, we should also consider server reconfiguration. (or at least decide whether this will be supported for server reconfiguration)

Good point. Do you know a technical limitation the prevents the original ServerPorts from being used for reconfiguration? Otherwise, should we use the original ones?

Copy link
Contributor

Choose a reason for hiding this comment

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

If this approach is taken, we should also consider server reconfiguration. (or at least decide whether this will be supported for server reconfiguration)

I think I may have misunderstood the original proposal by @ikhoon . Storing ServerPort into the channel attribute and querying the VirtualHost sounds like a good approach to me 👍

Copy link
Contributor

Choose a reason for hiding this comment

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

On second thought, we could allow ServerPort to update the actual port once when the server obtains an ephemeral port. In that case, storing ServerPort into the channel would be unnecessary.

ServerPort serverPort = ...;
// This is allowed when the port is 0 and `actualPort` hasn't been set yet.
serverPort.actualPort(...)

Copy link
Author

Choose a reason for hiding this comment

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

Thank you for the suggestion!

I'll implement the actualPort approach. I plan to add an actualPort field to ServerPort that can only be set once when the port is 0. After successful port binding, Server will call port.actualPort(boundPort) to store the assigned ephemeral port. Then findVirtualHost will match requests using the serverPort.actualPort() value. This way, storing ServerPort in channel attributes becomes unnecessary and the design is simpler.

Does this approach look correct?

Copy link
Contributor

Choose a reason for hiding this comment

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

Yes, that’s what I was thinking.

assertThat(actualPort1).isGreaterThan(0);
assertThat(actualPort2).isGreaterThan(0);
assertThat(actualPort1).isNotEqualTo(actualPort2);

Copy link
Contributor

Choose a reason for hiding this comment

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

Given that the hostnamePattern is used extensively, do you mind also verifying hostnamePattern is set correctly here? (It would also help users if VirtualHost#port also returns the resolved port so that users can be aware of which virtual host is served by which port)

Copy link
Author

Choose a reason for hiding this comment

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

Thanks for the feedback! I'll add assertions to verify that the hostnamePattern is set correctly.

Regarding VirtualHost#port() returning the resolved port, I agree this is necessary and will include the change to ensure the feature is fully integrated. I'll make sure to have this updated along with the assertions.

Copy link
Author

Choose a reason for hiding this comment

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

It would also help users if VirtualHost#port also returns the resolved port so that users can be aware of which virtual host is served by which port

Thank you for the feedback regarding VirtualHost#port().

Currently, port() returns 0 for ephemeral ports because it only returns the final int port assigned at build time. I noticed that hostnamePattern() already handles this by referencing serverPort.localAddress() to return the resolved port.

I propose updating port() to follow the same pattern:

VirtualHost.java

public int port() {
    if (serverPort != null && serverPort.localAddress() != null) {
        final int actualPort = serverPort.localAddress().getPort();
        if (actualPort > 0) {
            return actualPort;
        }
    }
    return port;
}

This change allows the test to verify the exact resolved port using a more descriptive assertion.

Updated Test Case:

final VirtualHost vh = server.config().virtualHosts().stream()
                             .filter(v -> v.serverPort() == port1)
                             .findFirst()
                             .orElseThrow(() -> new NoSuchElementException("VirtualHost for port1 not found"));
assertThat(vh.port())
        .as("VirtualHost bound to %s should have resolved port %d", port1, actualPort1)
        .isEqualTo(actualPort1);

Does this approach look reasonable to you, or is there a more preferred way in Armeria to handle this? I'm also open to any other suggestions if you think there's a better architectural direction for resolving ports in VirtualHost. If you agree, I'm happy to include this enhancement in the current PR.

Thank you!

Copy link
Author

Choose a reason for hiding this comment

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

@jrhee17
While I was drafting my response, I noticed a new review has been submitted. I will carefully review the feedback and update the PR accordingly. Thank you for the guidance!

…lookup

- Add actualPort field and methods to ServerPort for storing actual bound port
- Add actualPortToVirtualHost map to DefaultServerConfig for O(1) lookup
- Update Server to set actualPort after binding and build port mapping
- Add tests for actualPort and multiple random port VirtualHosts
Copy link

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 2

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (1)
core/src/main/java/com/linecorp/armeria/server/Server.java (1)

800-825: ⚠️ Potential issue | 🔴 Critical

Add build-time validation to prevent virtualHost(ServerPort) with port groups or ensure setActualPort() is called for port group reuse scenarios.

When a ServerPort belongs to a port group and reuses a previously bound port in NextServerPortStartListener (lines 900-904), the temporary ServerPort created doesn't have setActualPort() called on the original config port. This causes buildActualPortMapping() to skip creating routing entries for ephemeral ports, breaking port-based virtual host routing silently.

Consider adding a build-time check in ServerBuilder to either:

  1. Reject configurations where virtualHost(ServerPort) uses a ServerPort with a portGroup, or
  2. Fix the runtime behavior to ensure setActualPort() is called for all reused ports in a port group
🤖 Fix all issues with AI agents
In `@core/src/main/java/com/linecorp/armeria/server/DefaultServerConfig.java`:
- Around line 405-423: ServerBuilder.virtualHost(ServerPort) currently accepts
any ServerPort but DefaultServerConfig.buildActualPortMapping only maps
ephemeral ports (where ServerPort.localAddress().getPort() == 0), so add a
validation in ServerBuilder.virtualHost(ServerPort) to enforce ephemeral ports:
check serverPort.localAddress().getPort() == 0 and throw an
IllegalArgumentException (or use Objects.requireNonNull/assert) with a clear
message if not ephemeral; reference ServerBuilder.virtualHost(ServerPort),
ServerPort.localAddress().getPort(), and
DefaultServerConfig.buildActualPortMapping to locate the related logic.

In `@core/src/main/java/com/linecorp/armeria/server/ServerPort.java`:
- Around line 249-260: Add the `@UnstableApi` annotation to the new public method
actualPort() in the ServerPort class to follow project guidelines for newly
added public APIs; update imports to include the UnstableApi annotation (e.g.,
com.linecorp.armeria.common.annotation.UnstableApi) and place the annotation
immediately above the actualPort() method signature so the method is marked as
unstable.
🧹 Nitpick comments (2)
core/src/main/java/com/linecorp/armeria/server/ServerPort.java (2)

270-277: setActualPort has a non-atomic check-then-act on a volatile field.

The guard checkState(this.actualPort == -1, ...) followed by this.actualPort = actualPort is not atomic. While this is package-private and currently only called from a single event-loop thread in ServerPortStartListener, a future caller from a different thread could trigger a race where two threads both see -1 and both proceed to write.

Consider either:

  • Documenting the single-thread assumption (e.g., @GuardedBy or a Javadoc note), or
  • Using a CAS via AtomicInteger / VarHandle for correctness.

337-345: toString() does not reflect actualPort or originalServerPort.

After binding, actualPort holds meaningful diagnostic information (the real ephemeral port). Including it in toString() would improve debuggability when inspecting bound ports in logs.

Comment on lines 405 to 423
/**
* Builds the O(1) lookup map from actual bound ports to VirtualHosts.
* This should be called after all ports are bound.
*/
void buildActualPortMapping() {
final Int2ObjectMap<VirtualHost> mapping = new Int2ObjectOpenHashMap<>();
for (VirtualHost vh : virtualHosts) {
final ServerPort sp = vh.serverPort();
if (sp != null && sp.localAddress().getPort() == 0) {
final int actualPort = sp.actualPort();
if (actualPort > 0) {
mapping.put(actualPort, vh);
}
}
}
if (!mapping.isEmpty()) {
actualPortToVirtualHost = mapping;
}
}
Copy link

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major

🧩 Analysis chain

🏁 Script executed:

# Find ServerBuilder and virtualHost method implementations
find . -name "ServerBuilder.java" -type f | head -5

Repository: line/armeria

Length of output: 123


🏁 Script executed:

# Search for virtualHost method with ServerPort parameter
rg -n "virtualHost.*ServerPort" --type=java -B2 -A10 | head -100

Repository: line/armeria

Length of output: 8958


🏁 Script executed:

# Search for ServerPort class definition
find . -name "ServerPort.java" -type f

Repository: line/armeria

Length of output: 120


🏁 Script executed:

# Look for port validation in ServerBuilder context
rg -n "ServerPort.*port.*0|ephemeral" --type=java -B2 -A2 | head -80

Repository: line/armeria

Length of output: 8867


🏁 Script executed:

# Check ServerPort implementation to understand the actual port handling
cat core/src/main/java/com/linecorp/armeria/server/ServerPort.java

Repository: line/armeria

Length of output: 12854


🏁 Script executed:

# Look at buildDomainMapping to understand the full context
sed -n '330,350p' core/src/main/java/com/linecorp/armeria/server/DefaultServerConfig.java

Repository: line/armeria

Length of output: 1158


🏁 Script executed:

# Check if there's any validation in ServerBuilder that would prevent non-ephemeral ServerPort
sed -n '1880,1920p' core/src/main/java/com/linecorp/armeria/server/ServerBuilder.java

Repository: line/armeria

Length of output: 1940


🏁 Script executed:

# Look for any documentation or comments about ServerPort usage constraints
rg -n "ServerPort.*ephemeral|virtualHost.*ServerPort" --type=java --type=mdx | grep -i "comment\|javadoc\|ephemeral\|doc" | head -20

Repository: line/armeria

Length of output: 83


🏁 Script executed:

# Check if there's any validation added in ServerBuilder for ephemeral ports
rg -n "virtualHost.*ServerPort|port.*0|ephemeral" core/src/main/java/com/linecorp/armeria/server/ServerBuilder.java -B3 -A3 | head -60

Repository: line/armeria

Length of output: 2365


🏁 Script executed:

# Check if buildActualPortMapping is tested with non-ephemeral ports
rg -n "buildActualPortMapping|ServerPort.*[89][0-9]{2,}" core/src/test/java/com/linecorp/armeria/server/ -B2 -A2 | head -80

Repository: line/armeria

Length of output: 1843


🏁 Script executed:

# Check what happens after buildActualPortMapping - how is actualPortToVirtualHost used?
sed -n '450,470p' core/src/main/java/com/linecorp/armeria/server/DefaultServerConfig.java

Repository: line/armeria

Length of output: 907


🏁 Script executed:

# Look for any VirtualHostBuilder validation that might restrict ServerPort to ephemeral
rg -n "class VirtualHostBuilder" core/src/main/java/com/linecorp/armeria/server/ -A 30 | head -80

Repository: line/armeria

Length of output: 3644


🏁 Script executed:

# Check if there are any test cases that try to use non-ephemeral ServerPort with virtualHost()
rg -n "virtualHost.*new ServerPort.*[89]" core/src/test/java/com/linecorp/armeria/server/ -B2 -A5

Repository: line/armeria

Length of output: 38


🏁 Script executed:

# Check the VirtualHostBuilder constructor to see if it validates ServerPort
sed -n '145,180p' core/src/main/java/com/linecorp/armeria/server/VirtualHostBuilder.java

Repository: line/armeria

Length of output: 1332


🏁 Script executed:

# Look for the build() method of VirtualHostBuilder to see if it creates VirtualHost with validation
rg -n "VirtualHostBuilder.*build\(\)|new VirtualHost" core/src/main/java/com/linecorp/armeria/server/VirtualHostBuilder.java -A 5 | head -60

Repository: line/armeria

Length of output: 2308


🏁 Script executed:

# Get the full build() method from VirtualHostBuilder
sed -n '1400,1530p' core/src/main/java/com/linecorp/armeria/server/VirtualHostBuilder.java

Repository: line/armeria

Length of output: 7353


🏁 Script executed:

# Check if there's validation for ephemeral port requirement for ServerPort-based VirtualHosts
rg -n "serverPort.*port.*0|ephemeral.*ServerPort|build\(\)" core/src/main/java/com/linecorp/armeria/server/VirtualHostBuilder.java -B3 -A3 | head -100

Repository: line/armeria

Length of output: 3314


🏁 Script executed:

# Check the VirtualHost constructor to see if it validates serverPort
rg -n "class VirtualHost|VirtualHost(" core/src/main/java/com/linecorp/armeria/server/VirtualHost.java -A 2 | head -50

Repository: line/armeria

Length of output: 166


🏁 Script executed:

# Check if there are any integration tests that would catch the unreachable VirtualHost issue
rg -n "virtualHost.*ServerPort.*8080|virtualHost.*new ServerPort.*[1-9]" core/src/test/java/com/linecorp/armeria/server/PortBasedVirtualHostTest.java -B2 -A5

Repository: line/armeria

Length of output: 38


🏁 Script executed:

# Check VirtualHost constructor
rg -n "public VirtualHost\(" core/src/main/java/com/linecorp/armeria/server/VirtualHost.java -A 30 | head -80

Repository: line/armeria

Length of output: 38


🏁 Script executed:

# Search for any validation logic in VirtualHost that checks serverPort
rg -n "serverPort.*port.*0|checkArgument.*serverPort|checkState.*serverPort" core/src/main/java/com/linecorp/armeria/server/VirtualHost.java -B2 -A2

Repository: line/armeria

Length of output: 38


🏁 Script executed:

# Check the actual port in the comment about ServerPort-based virtual hosts
sed -n '1860,1900p' core/src/main/java/com/linecorp/armeria/server/ServerBuilder.java

Repository: line/armeria

Length of output: 1793


ServerBuilder.virtualHost(ServerPort) must validate that the port is ephemeral (port 0).

The Javadoc and design explicitly state this API is for "random port (port 0)", and buildActualPortMapping only handles ephemeral ports where sp.localAddress().getPort() == 0. However, the API accepts any ServerPort without validation. A user calling virtualHost(new ServerPort(8080, HTTP)) would create a silently unreachable VirtualHost—it would be skipped by both the domain mapping (line 338) and the actual port mapping (requires port == 0).

Add a port validation check in ServerBuilder.virtualHost(ServerPort) to enforce that serverPort.localAddress().getPort() == 0, or document this constraint with a runtime assertion.

🤖 Prompt for AI Agents
In `@core/src/main/java/com/linecorp/armeria/server/DefaultServerConfig.java`
around lines 405 - 423, ServerBuilder.virtualHost(ServerPort) currently accepts
any ServerPort but DefaultServerConfig.buildActualPortMapping only maps
ephemeral ports (where ServerPort.localAddress().getPort() == 0), so add a
validation in ServerBuilder.virtualHost(ServerPort) to enforce ephemeral ports:
check serverPort.localAddress().getPort() == 0 and throw an
IllegalArgumentException (or use Objects.requireNonNull/assert) with a clear
message if not ephemeral; reference ServerBuilder.virtualHost(ServerPort),
ServerPort.localAddress().getPort(), and
DefaultServerConfig.buildActualPortMapping to locate the related logic.

- Add @UnstableApi annotation to ServerPort.actualPort() per Armeria conventions
- Add fixedPortVirtualHostWithServerPort() test to verify virtualHost(ServerPort) works correctly
}
}
if (!mapping.isEmpty()) {
actualPortToVirtualHost = mapping;
Copy link
Contributor

Choose a reason for hiding this comment

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

Should we defer building domain and port mapping until the actual port is resolved instead of adding a new maping?

Copy link
Author

@junjunclub junjunclub Feb 10, 2026

Choose a reason for hiding this comment

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

That's a good point. Deferring virtualHostAndPortMapping until after ports are bound would let us unify the two maps into one. The tradeoff is that virtualHostAndPortMapping is currently final and would need to become volatile, and the buildDomainAndPortMapping() logic would need to incorporate actual port resolution. I'm happy to refactor if you prefer a unified approach.

* or {@code null} if this is an original port.
*/
@Nullable
ServerPort originalServerPort() {
Copy link
Contributor

Choose a reason for hiding this comment

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

Question) Is this only used for testing?

Copy link
Author

@junjunclub junjunclub Feb 10, 2026

Choose a reason for hiding this comment

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

Yes, with the actualPortToVirtualHost map replacing the old loop in findVirtualHost(), originalServerPort() is no longer used in production code only in tests. I can remove it if it's no longer needed.

Copy link
Contributor

@jrhee17 jrhee17 left a comment

Choose a reason for hiding this comment

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

Changes look reasonable to me 👍

if (serverPort != null && serverPort.localAddress() != null) {
final int actualPort = serverPort.localAddress().getPort();
if (actualPort > 0) {
return originalDefaultHostname + ':' + actualPort;
Copy link
Contributor

Choose a reason for hiding this comment

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

nit; could this be cached? Depending on the call path, this may be called for every request. Ditto for hostnamePattern.

Copy link
Author

Choose a reason for hiding this comment

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

Thank you for the suggestion! Since actualPort is fixed after server start, caching the computed string would avoid unnecessary allocation on repeated calls. I'll add cached fields and compute them lazily on the first access after the port is bound. 👍👍

* Returns the name of the default host.
*/
public String defaultHostname() {
if (serverPort != null && serverPort.localAddress() != null) {
Copy link
Contributor

Choose a reason for hiding this comment

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

Question) Can localAddress be null?

Suggested change
if (serverPort != null && serverPort.localAddress() != null) {
if (serverPort != null) {

Copy link
Author

Choose a reason for hiding this comment

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

Thank you for catching this. You're right, localAddress() returns a final field that is initialized with requireNonNull in the constructor, so it can never be null. I'll remove the unnecessary null check.

- Defer VirtualHost domain/port mapping until after ports are bound,
  unifying virtualHostAndPortMapping and actualPortToVirtualHost into
  a single volatile map that is rebuilt via rebuildDomainAndPortMapping()
- Remove originalServerPort from ServerPort as it is no longer used
  after the mapping unification
- Cache defaultHostname() and hostnamePattern() results in VirtualHost
  to avoid repeated String allocation on every call
- Remove unnecessary null check on ServerPort.localAddress() which is
  guaranteed non-null by requireNonNull in constructor
Copy link

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

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

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (2)
core/src/main/java/com/linecorp/armeria/server/DefaultServerConfig.java (2)

447-465: ⚠️ Potential issue | 🟠 Major

Multiple reads of volatile virtualHostAndPortMapping — use a local snapshot.

findVirtualHost reads the volatile field on lines 449, 453, and 463 without capturing it in a local variable. If rebuildDomainAndPortMapping() replaces the reference between reads, different reads may see different map instances, leading to inconsistent lookups within a single call.

Proposed fix
     `@Override`
     public VirtualHost findVirtualHost(String hostname, int port) {
-        if (virtualHostAndPortMapping == null) {
+        final Int2ObjectMap<Mapping<String, VirtualHost>> mapping = virtualHostAndPortMapping;
+        if (mapping == null) {
             return defaultVirtualHost;
         }
 
-        final Mapping<String, VirtualHost> virtualHostMapping = virtualHostAndPortMapping.get(port);
+        final Mapping<String, VirtualHost> virtualHostMapping = mapping.get(port);
         if (virtualHostMapping != null) {
             final VirtualHost virtualHost = virtualHostMapping.map(hostname + ':' + port);
             // Exclude the default virtual host from port-based virtual hosts.
             if (virtualHost != defaultVirtualHost) {
                 return virtualHost;
             }
         }
 
         // No port-based virtual host is configured. Look for named-based virtual host.
-        final Mapping<String, VirtualHost> nameBasedMapping = requireNonNull(virtualHostAndPortMapping.get(-1));
+        final Mapping<String, VirtualHost> nameBasedMapping = requireNonNull(mapping.get(-1));
         return nameBasedMapping.map(hostname);
     }

439-445: ⚠️ Potential issue | 🟡 Minor

Same volatile read issue in deprecated findVirtualHost(String).

This method also reads virtualHostAndPortMapping twice without a local snapshot (lines 440 and 443). Apply the same local-variable pattern for consistency.

Proposed fix
     `@Override`
     `@Deprecated`
     public VirtualHost findVirtualHost(String hostname) {
-        if (virtualHostAndPortMapping == null) {
+        final Int2ObjectMap<Mapping<String, VirtualHost>> mapping = virtualHostAndPortMapping;
+        if (mapping == null) {
             return defaultVirtualHost;
         }
-        final Mapping<String, VirtualHost> virtualHostMapping = virtualHostAndPortMapping.get(-1);
+        final Mapping<String, VirtualHost> virtualHostMapping = mapping.get(-1);
         return virtualHostMapping.map(hostname);
     }
🧹 Nitpick comments (1)
core/src/test/java/com/linecorp/armeria/server/PortBasedVirtualHostTest.java (1)

241-293: Consider using ServerPort.actualPort() directly for clarity.

The serverPortBasedVirtualHostRoutesCorrectly and multipleRandomPortVirtualHosts tests access port1.actualPort() directly, which is cleaner than sorting activePorts(). This test could be simplified similarly, though it still works correctly as-is.

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.

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))

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Armeria per-port VirtualHost and random port are incompatible

4 participants