Skip to content

Support nested context paths.#5846

Merged
minwoox merged 14 commits intoline:mainfrom
chickenchickenlove:2407428-GH-5758
Dec 31, 2025
Merged

Support nested context paths.#5846
minwoox merged 14 commits intoline:mainfrom
chickenchickenlove:2407428-GH-5758

Conversation

@chickenchickenlove
Copy link
Copy Markdown
Contributor

@chickenchickenlove chickenchickenlove commented Jul 30, 2024

Motivation:

  • Currently, armeria supports only 1-depth context paths. Sometimes, user want deeper context paths than 1-depth when they use contextPaths().

Modifications:

  • Make ContextPathServiceBuilder tree to support nested context paths.
  • Add public functions.
    • before() is for going back previous node.
    • contextPath() is for adding context paths and making child ContextPathServiceBuilder
  • Add package-private and private functions.
    • parent(): To give Child contextPathServiceBuilder parent object. because of this, child can return parent object when and() is called.
    • virtualHostbuilder(): to relay their context via ContextPathServiceBuilder tree.
    • mergeContextPaths(): to merge previous context paths and current context paths.

Example

Result:

sb.contextPath("/rest")
      .contextPath("/catalog")
          .service("/product", new GetProductService())
          .service("/products", new ProductsHandler())
          .before()
      .contextPath("/cart")
          .contextPath("/foo")
               .contextPath("/bar")
                    .service("/checkout", new CheckoutService());
                    .and()
  .contextPath("/gql")
      .service("/catalog", new GraphQLService());

/rest/catalog/product => getProductService
/rest/catalog/products => productsHandler
/rest/cart/foo/bar/checkout => checkoutService
/gql/catalog => GraphQLService

@chickenchickenlove
Copy link
Copy Markdown
Contributor Author

I would like to receive feedback on whether this PR is heading in the right direction, as it is currently a draft.

After fix direction, I will write unit tests and Java docs based on the BaseContextPathsTest code.

When you have time, please take a look 🙇‍♂️

@github-actions
Copy link
Copy Markdown
Contributor

github-actions Bot commented Jul 30, 2024

@ikhoon
Copy link
Copy Markdown
Contributor

ikhoon commented Aug 8, 2024

I didn't see that a fluent style is good for nested context paths in terms of API design. Users may find it difficult to know the current depth and parent trees and children trees.

How about taking a lambda expression as a second parameter to leverage Kotlin trailing lamdas?
https://kotlinlang.org/docs/lambdas.html#passing-trailing-lambdas

I imagined the following style API. We used contextPath() at the top level but I am not sure if the naming contextPath is also good for the nested routes.

Server
  .builder()
  .contextPath("/rest") {
     contextPath("/catalog") {
        service("/product", new GetProductService())
        service("/products", new ProductsHandler())
     }
  }
  .contextPath("/cart") {
     contextPath("/foo") {
        contextPath("/bar") {
           service("/checkout", new CheckoutService())
        }
     }
  }
  .contextPath("/gql") {
      service("/catalog", new GraphQLService())
   }

Related work: https://ktor.io/docs/server-routing.html#nested_routes

@chickenchickenlove
Copy link
Copy Markdown
Contributor Author

@ikhoon nim, Thanks for your guidelines.
I didn't know about KTOR. i will take a look and follow how KTOR supports nested context path.

@chickenchickenlove
Copy link
Copy Markdown
Contributor Author

chickenchickenlove commented Aug 25, 2024

@ikhoon nim, i change code style from fluent style to nested lambda expression.
For example, we can use feature of nested context like this.

// Without virtual host
        sb.baseContextPath("/api")
          .contextPath("/context-path/a1", "/context-path/a2")
          .nestedContext()
          .contextPaths(Set.of("/b1", "/b2"), ctx1 -> ctx1  // ctx1 scope start (lambda function)
                  .annotatedService(new Object() { ... })
                  .service("/my-service", new HttpService() { ... })
                  .contextPaths(Set.of("/c1", "/c2"), ctx2 -> ctx2 // ctx2 scope start (lambda function)
                          .service("/my-service1", new HttpService() { ... })
                          .contextPaths(Set.of(...), ctx3 -> ctx3  // ctx3 scope start (lambda function)
                                   .service("/...", new HttpService() { ... })
                                   ) // ctx3 scope end.
                           ) // ctx2 scope end.                   
           ) // ctx1 scope end.
          .contextPaths(Set.of("/b3", "/b4"), ctx11 -> ctx11
                  .service("/my-service", new HttpService() { ... });

// With virtual host.
       sb.virtualHost("foo.com")
          .contextPath("/virtual-foo")
          .nestedContext()
          .contextPaths(Set.of("/a1", "/a2"), ctx -> ctx
                  .service("/my-service1", new HttpService() { ... }));

To get detail code, you can refer to this java code.
Also, i create pytest code to test whether endpoint is valid, corresponding to java code above.

Java does not support lambda functions as robustly as Kotlin does.
IMHO, it would be difficult to implement the API in the same way perfectly as you imagined 🤔.
Like Kotlin trailing lamdas, i introduce lambda function as second parameter. (as you recommended!)
lambda functions represents the context scope and incorporate a bit of a Fluent API style within it.

It may not be exactly the same as the API you imagined, but since we can represent the scope within a lambda function, I believe it will be more user-friendly than my initial commit. also, java cannot perfectly match to Kotlin trailing lambdas, i guess 😢.

When you have time, please take another look. 🙇‍♂️

@github-actions github-actions Bot added the Stale label Sep 25, 2024
@chickenchickenlove
Copy link
Copy Markdown
Contributor Author

@ikhoon nim, gently ping.
When you have time, please take a look 🙇‍♂️
Or would you prefer to not proceed with this PR any further, in which case I'd be happy to close this PR.

@github-actions github-actions Bot removed the Stale label Apr 29, 2025
@ikhoon
Copy link
Copy Markdown
Contributor

ikhoon commented Apr 29, 2025

 sb.baseContextPath("/api")
   .contextPath("/context-path/a1", "/context-path/a2")
   .nestedContext()
   .contextPaths(Set.of("/b1", "/b2"), ctx1 -> ctx1  // ctx1 scope start (lambda function)

Is it possible to build nested context paths without using .nestedContext()? I would like the API to be designed in a form close to a DSL.

@chickenchickenlove
Copy link
Copy Markdown
Contributor Author

@ikhoon nim, sure.
I fixed the codes.
Two styles are supported.

I updated example codes and API test codes..

Style1

It starts with contextPath(Iterable<String> paths).

      sb.baseContextPath("/api")
        .contextPath("/context-path/a1", "/context-path/a2")
        .contextPaths(Set.of("/b1", "/b2"), ctx1 -> ctx1  // ctx1 scope start (lambda function)
                .annotatedService(new Object() { ... })
                .service("/my-service", new HttpService() { ... })
                .contextPaths(Set.of("/c1", "/c2"), ctx2 -> ctx2 // ctx2 scope start (lambda function)
                        .service("/my-service1", new HttpService() { ... })
                        .contextPaths(Set.of(...), ctx3 -> ctx3  // ctx3 scope start (lambda function)
                                 .service("/...", new HttpService() { ... })
                                 ) // ctx3 scope end.
                       ) // ctx2 scope end.                   
         ) // ctx1 scope end.
        .contextPaths(Set.of("/b3", "/b4"), ctx11 -> ctx11
                .service("/my-service", new HttpService() { ... });

Style2

It starts with contextPath(Iterable<String> paths, Consumer<ContextPathServicesBuilder> context).

      sb.baseContextPath("/api")
        .contextPath(Set.of("/b1", "/b2"), ctx1 -> ctx1  // ctx1 scope start (lambda function)
                .annotatedService(new Object() { ... })
                .service("/my-service", new HttpService() { ... })
                .contextPaths(Set.of("/c1", "/c2"), ctx2 -> ctx2 // ctx2 scope start (lambda function)
                        .service("/my-service1", new HttpService() { ... })
                        .contextPaths(Set.of(...), ctx3 -> ctx3  // ctx3 scope start (lambda function)
                                 .service("/...", new HttpService() { ... })
                                 ) // ctx3 scope end.
                       ) // ctx2 scope end.                   
         ) // ctx1 scope end.
        .contextPaths(Set.of("/b3", "/b4"), ctx11 -> ctx11
                .service("/my-service", new HttpService() { ... });

When you have time, PTAL 🙇‍♂️

@github-actions github-actions Bot added the Stale label Jun 1, 2025
@ikhoon
Copy link
Copy Markdown
Contributor

ikhoon commented Jun 19, 2025

Could you explain the difference between contextPath() and contextPaths()? The method names alone don't make it clear.

@github-actions github-actions Bot removed the Stale label Jun 22, 2025
@chickenchickenlove
Copy link
Copy Markdown
Contributor Author

chickenchickenlove commented Jul 19, 2025

@ikhoon nim, sorry to late.
I updated interface.

For ContextBuilder

      sb.baseContextPath("/api")
        .toContextBuilder()
        .contextPath(Set.of("/b1", "/b2"), ctx1 -> ctx1  // ctx1 scope start (lambda function)
                .annotatedService(new Object() { ... })
                .service("/my-service", new HttpService() { ... })
                .contextPath(Set.of("/c1", "/c2"), ctx2 -> ctx2 // ctx2 scope start (lambda function)
                        .service("/my-service1", new HttpService() { ... })
                        .contextPath(Set.of(...), ctx3 -> ctx3  // ctx3 scope start (lambda function)
                                 .service("/...", new HttpService() { ... })
                                 ) // ctx3 scope end.
                       ) // ctx2 scope end.                   
         ) // ctx1 scope end.
        .contextPath(Set.of("/b3", "/b4"), ctx11 -> ctx11
                .service("/my-service", new HttpService() { ... });

Currently, baseContextPath() returns ServerBuilder.
I don't want to break backward compatiablity, so I added new function toContextBuilder() to retrieve ContextPathServicesBuilder.

Then, we can use contextPath() for nested path.

For VirtuaHostlBuilder

      sb.virtualHost("foo.com")
          .contextPath(Set.of("/b1", "/b2"), ctx1 -> ctx1  // ctx1 scope start (lambda function)
                .annotatedService(new Object() { ... })
                .service("/my-service", new HttpService() { ... })
                .contextPath(Set.of("/c1", "/c2"), ctx2 -> ctx2 // ctx2 scope start (lambda function)
                        .service("/my-service1", new HttpService() { ... })
                        .contextPath(Set.of(...), ctx3 -> ctx3  // ctx3 scope start (lambda function)
                                 .service("/...", new HttpService() { ... })
                                 ) // ctx3 scope end.
                       ) // ctx2 scope end.                   
         ) // ctx1 scope end.
          .contextPath(Set.of("/b3", "/b4"), ctx11 -> ctx11
                  .service("/my-service", new HttpService() { ... });

When you have time, Please take another look. 🙇‍♂️

@github-actions github-actions Bot added the Stale label Aug 19, 2025
@codecov
Copy link
Copy Markdown

codecov Bot commented Aug 19, 2025

Codecov Report

❌ Patch coverage is 97.01493% with 2 lines in your changes missing coverage. Please review.
✅ Project coverage is 74.30%. Comparing base (8150425) to head (ef78294).
⚠️ Report is 297 commits behind head on main.

Files with missing lines Patch % Lines
.../server/VirtualHostContextPathServicesBuilder.java 88.88% 1 Missing and 1 partial ⚠️
Additional details and impacted files
@@             Coverage Diff              @@
##               main    #5846      +/-   ##
============================================
- Coverage     74.46%   74.30%   -0.16%     
- Complexity    22234    23466    +1232     
============================================
  Files          1963     2105     +142     
  Lines         82437    87926    +5489     
  Branches      10764    11519     +755     
============================================
+ Hits          61385    65335    +3950     
- Misses        15918    17107    +1189     
- Partials       5134     5484     +350     

☔ 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.

@github-actions github-actions Bot removed the Stale label Aug 20, 2025
@github-actions github-actions Bot added the Stale label Sep 28, 2025
@chickenchickenlove
Copy link
Copy Markdown
Contributor Author

@ikhoon nim
Sorry to bother you.
When you have time, Please take another look. 🙇‍♂️

* @return
*/
@UnstableApi
public ContextPathServicesBuilder toContextBuilder() {
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Question) Could we add a new API that directly builds services under a context path with a customizer instead of toContextBuilder()?

public ServerBuilder contextPath(Iterable<String> contextPaths,
                                 Consumer<ContextPathServicesBuilder> customizer) {
    requireNonNull(contextPaths, "contextPaths");
    customizer.accept(contextPath(contextPaths));
    return this;
}

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

It works. applied.

@github-actions github-actions Bot removed the Stale label Oct 19, 2025
@chickenchickenlove
Copy link
Copy Markdown
Contributor Author

Hi, @ikhoon nim.
Sorry to bother you.
When you have time, could you take a look?

@chickenchickenlove
Copy link
Copy Markdown
Contributor Author

@ikhoon , HI!
Thanks for your comments.
I made new commits based on your comments. (Add test case, Java Docs)
Also, I found the defects in RejectRouterHandler.FAIL and issued it (#6498) and made a patch(#6499)

If #6499 is merged, I can write test code for testing duplicated routing rules in nested context path.

When you have time, please take another look. 🙇‍♂️

final Set<String> mergedContextPaths = new HashSet<>();
for (String currentContextPath : contextPaths()) {
for (String childContextPath : paths) {
RouteUtil.ensureAbsolutePath(childContextPath, "contextPath");
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Should we validate the paths when they are set in the builder methods?

Copy link
Copy Markdown
Contributor Author

@chickenchickenlove chickenchickenlove Nov 18, 2025

Choose a reason for hiding this comment

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

@ikhoon Thanks for your comments!
IMHO, It is not enough, so I think we must validate the paths before they are merged.
If we don't validate the context path before they are merged,
the codes below will not be failed.
Builder will see /hellorelative and it don't violate absolute path.

    @Test
    void relativeNestedContextPathShouldBeFailed() {
        final ServerBuilder serverBuilder = new ServerBuilder().baseContextPath("/api");
        assertThatThrownBy(() -> {
            serverBuilder.contextPath(ImmutableSet.of("/hello"), ctx1 -> {
                ctx1.contextPath(ImmutableSet.of("relative"), ctx2 -> {
                    ctx2.serviceUnder("/hello", (ctx, req) -> HttpResponse.of(HttpStatus.OK));
                });
            });
        }).isInstanceOf(IllegalArgumentException.class);
    }

So, I think keeping RouteUtil.ensureAbsolutePath(...) in mergeContextPaths(...) is reasonable.
What do you think?

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

My intent was to promptly validate relative if the path is absolute when ctx1.contextPath(ImmutableSet.of("relative") is called.
See
https://armeria.dev/community/developer-guide#validate to learn Armeria code convention.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

Sorry, I missed this comments 🙇‍♂️
I addressed it.

Comment thread core/src/main/java/com/linecorp/armeria/server/ContextPathServicesBuilder.java Outdated
});
});
})
.contextPath("/single-path1", ctx1 -> {
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Optional) It would be nice if we had some Kotlin tests.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

OK, Let me try!

If #6499 is merged, I can write test code for testing duplicated routing rules in nested context path.

I will create bunch of test codes to validate duplicate nested context path.
At that time, I will create some kotlin tests as well!

@chickenchickenlove
Copy link
Copy Markdown
Contributor Author

@ikhoon nim
I made a new commit for test codes.
When you have time, please take another look. 🙇‍♂️

Copy link
Copy Markdown
Contributor

@ikhoon ikhoon left a comment

Choose a reason for hiding this comment

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

Thanks, @chickenchickenlove! 👍🚀

final Set<String> mergedContextPaths = new HashSet<>();
for (String currentContextPath : contextPaths()) {
for (String childContextPath : paths) {
RouteUtil.ensureAbsolutePath(childContextPath, "contextPath");
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

My intent was to promptly validate relative if the path is absolute when ctx1.contextPath(ImmutableSet.of("relative") is called.
See
https://armeria.dev/community/developer-guide#validate to learn Armeria code convention.

Comment on lines +124 to +125
contextPath(ImmutableSet.of(path), customizer);
return this;
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

nit

Suggested change
contextPath(ImmutableSet.of(path), customizer);
return this;
return contextPath(ImmutableSet.of(path), customizer);

/**
* Applies the specified {@code customizer} to a {@link ContextPathServicesBuilder} rooted at the
* given {@code contextPaths} of the default {@link VirtualHost}.
* @param contextPaths the context paths to group services under; must be non-empty and absolute
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

must be non-empty

This constrain doesn't seem to be applied.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

I make it constrain.
Please take a look!

Comment on lines +128 to +129
contextPath(ImmutableSet.of(path), customizer);
return this;
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Suggested change
contextPath(ImmutableSet.of(path), customizer);
return this;
return contextPath(ImmutableSet.of(path), customizer);

@coderabbitai
Copy link
Copy Markdown
Contributor

coderabbitai Bot commented Nov 27, 2025

Walkthrough

Adds nested context-path configuration overloads that accept a Consumer to create and customize child builders rooted at combined context paths; includes absolute/non-empty path validation, new abstract APIs, concrete implementations on server/virtual-host builders, and comprehensive Java/Kotlin tests.

Changes

Cohort / File(s) Summary
Abstract base API
core/src/main/java/com/linecorp/armeria/server/AbstractContextPathServicesBuilder.java
Adds two abstract method signatures: contextPath(Iterable<String> paths, Consumer<SELF> customizer) and contextPath(String path, Consumer<SELF> customizer); exposes helper accessors and references merged context-path logic.
Server builder
core/src/main/java/com/linecorp/armeria/server/ServerBuilder.java
Adds contextPath(Iterable<String>, Consumer<ContextPathServicesBuilder>) and convenience contextPath(String, Consumer<...>) overloads; validate inputs, ensure absolute paths, delegate to child context-path builders and apply customizer.
Virtual-host builder
core/src/main/java/com/linecorp/armeria/server/VirtualHostBuilder.java
Adds contextPath(Iterable<String>, Consumer<VirtualHostContextPathServicesBuilder>) and convenience contextPath(String, Consumer<...>) overloads with input validation and delegation to child builders.
Context-path builders implementations
core/src/main/java/com/linecorp/armeria/server/ContextPathServicesBuilder.java, core/src/main/java/com/linecorp/armeria/server/VirtualHostContextPathServicesBuilder.java
Implement iterable and single-path overloads: null checks, per-path absolute validation via RouteUtil.ensureAbsolutePath, create child builders using merged context paths, apply provided Consumer, and return parent for chaining.
Tests (Java)
core/src/test/java/com/linecorp/armeria/server/HttpServerNestedContextPathTest.java
Adds comprehensive tests exercising nested context-path routing, virtual-host scenarios, validation failures (empty/relative paths), duplicate-route detection, exact vs prefix matching, and route path generation (no double-slash).
Tests (Kotlin)
kotlin/src/test/kotlin/com/linecorp/armeria/server/kotlin/HttpServerNestedContextPathTest.kt
Adds Kotlin test coverage mirroring Java tests with in-memory route maps, virtual-host checks, validation/error cases, duplicate path handling, and helper data class for expected responses.

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~45 minutes

  • Focus review on: mergedContextPaths behavior and correctness when combining multiple path sets.
  • Verify consistent use of RouteUtil.ensureAbsolutePath and Iterables/ImmutableSet handling.
  • Pay attention to tests that assert duplicate-route behavior and route-count expectations.

Suggested reviewers

  • trustin
  • ikhoon
  • jrhee17
  • minwoox

Poem

🐰 I stitched paths branch by branch and hop,
Builders nested, routes no longer stop,
Consumers guide where children play,
No double-slash along the way,
Hooray — routes tidy, tidy crop! 🥕

Pre-merge checks and finishing touches

❌ Failed checks (1 warning)
Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 12.90% which is insufficient. The required threshold is 80.00%. You can run @coderabbitai generate docstrings to improve docstring coverage.
✅ Passed checks (4 passed)
Check name Status Explanation
Title check ✅ Passed The title 'Support nested context paths' directly and clearly summarizes the main change: introducing nested context path support to Armeria's builder API.
Description check ✅ Passed The PR description is well-documented with clear motivation, modifications, examples, and expected outcomes, all directly related to the nested context paths feature.
Linked Issues check ✅ Passed The changes fully address the requirements in issue #5758 by implementing nested context path support with Consumer-based APIs across ServerBuilder, VirtualHostBuilder, and their context path variants, enabling hierarchical service organization.
Out of Scope Changes check ✅ Passed All code changes are focused on implementing nested context path support as specified in issue #5758; no unrelated modifications or out-of-scope changes were introduced.
✨ Finishing touches
  • 📝 Generate docstrings
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Post copyable unit tests 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
Copy Markdown
Contributor

@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

🧹 Nitpick comments (4)
core/src/test/java/com/linecorp/armeria/server/HttpServerNestedContextPathTest.java (2)

347-362: Consider adding descriptive assertion messages for easier debugging.

When assertions fail within the loop, identifying the failing URL requires additional investigation. Adding context to assertions would improve test maintainability.

-            assertThat(res.status()).isEqualTo(expectedRes.status);
+            assertThat(res.status())
+                    .as("Status mismatch for URL: %s", url.getKey())
+                    .isEqualTo(expectedRes.status);

             if (res.status() == HttpStatus.OK) {
-                assertThat(res.content(StandardCharsets.UTF_8)).isEqualTo(expectedRes.body);
+                assertThat(res.content(StandardCharsets.UTF_8))
+                        .as("Body mismatch for URL: %s", url.getKey())
+                        .isEqualTo(expectedRes.body);
             }

484-527: Naming convention inconsistency with other test methods.

These test methods use snake_case while other methods in this file use camelCase. Consider aligning the naming convention for consistency.

-    void duplicate_path_should_be_failed_in_2depth_nested_context_with_server_builder() {
+    void duplicatePathShouldBeFailedIn2DepthNestedContextWithServerBuilder() {
-    void duplicate_path_should_be_failed_in_1depth_nested_context_with_server_builder() {
+    void duplicatePathShouldBeFailedIn1DepthNestedContextWithServerBuilder() {
core/src/main/java/com/linecorp/armeria/server/AbstractContextPathServicesBuilder.java (1)

23-25: Nested contextPath API and helpers look sound; fix minor Javadoc nit

The new helpers (virtualHostBuilder(), parent(), mergedContextPaths(...)) plus the abstract contextPath(Iterable<String>, Consumer<SELF>) / contextPath(String, Consumer<SELF>) give a clean foundation for the concrete builders to build the context-path tree, and the Cartesian-product merge via HashSet is appropriate.

One small follow-up:

  • In the Javadoc of the contextPath(String path, Consumer<SELF> customizer) overload, the @throws clause still refers to paths instead of path, which can be confusing when reading the API docs.

Optionally, you could also consider relaxing the consumer type to Consumer<? super SELF> in the abstract signature to allow slightly more flexible callers, but that’s not required for correctness here.

Also applies to: 415-432, 459-469

core/src/main/java/com/linecorp/armeria/server/VirtualHostBuilder.java (1)

465-503: VirtualHost-level contextPath customizer overloads are consistent; explicit empty-check is optional

The new contextPath(Iterable<String>, Consumer<VirtualHostContextPathServicesBuilder>) and single-path overload:

  • Null-check both parameters.
  • Reuse the existing contextPath(Iterable<String>) factory to obtain a VirtualHostContextPathServicesBuilder, so non-empty and absolute-path constraints are enforced by the underlying builder construction.
  • Return this for fluent chaining, matching the rest of the API.

If you want error reporting to mirror ServerBuilder.contextPath(Iterable, Consumer), you could add an explicit Iterables.isEmpty-style check here to fail early on empty iterables, but current behavior is already correct and throws an IllegalArgumentException via the child builder constructor.

📜 Review details

Configuration used: CodeRabbit UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 2bd1e2d and 0126a4d.

📒 Files selected for processing (7)
  • core/src/main/java/com/linecorp/armeria/server/AbstractContextPathServicesBuilder.java (2 hunks)
  • core/src/main/java/com/linecorp/armeria/server/ContextPathServicesBuilder.java (2 hunks)
  • core/src/main/java/com/linecorp/armeria/server/ServerBuilder.java (3 hunks)
  • core/src/main/java/com/linecorp/armeria/server/VirtualHostBuilder.java (1 hunks)
  • core/src/main/java/com/linecorp/armeria/server/VirtualHostContextPathServicesBuilder.java (2 hunks)
  • core/src/test/java/com/linecorp/armeria/server/HttpServerNestedContextPathTest.java (1 hunks)
  • kotlin/src/test/kotlin/com/linecorp/armeria/server/kotlin/HttpServerNestedContextPathTest.kt (1 hunks)
🧰 Additional context used
🧬 Code graph analysis (5)
core/src/main/java/com/linecorp/armeria/server/VirtualHostBuilder.java (2)
core/src/main/java/com/linecorp/armeria/server/ContextPathServicesBuilder.java (1)
  • UnstableApi (43-132)
core/src/main/java/com/linecorp/armeria/server/VirtualHostContextPathServicesBuilder.java (1)
  • UnstableApi (44-136)
kotlin/src/test/kotlin/com/linecorp/armeria/server/kotlin/HttpServerNestedContextPathTest.kt (1)
core/src/test/java/com/linecorp/armeria/server/HttpServerNestedContextPathTest.java (1)
  • StatusCodeAndBody (577-590)
core/src/test/java/com/linecorp/armeria/server/HttpServerNestedContextPathTest.java (2)
junit5/src/main/java/com/linecorp/armeria/testing/junit5/server/ServerExtension.java (1)
  • ServerExtension (49-405)
kotlin/src/test/kotlin/com/linecorp/armeria/server/kotlin/HttpServerNestedContextPathTest.kt (3)
  • VIRTUAL_HOSTNAME (45-669)
  • configure (53-187)
  • status (381-384)
core/src/main/java/com/linecorp/armeria/server/VirtualHostContextPathServicesBuilder.java (1)
core/src/main/java/com/linecorp/armeria/server/ContextPathServicesBuilder.java (1)
  • UnstableApi (43-132)
core/src/main/java/com/linecorp/armeria/server/ServerBuilder.java (3)
core/src/main/java/com/linecorp/armeria/server/ContextPathServicesBuilder.java (1)
  • UnstableApi (43-132)
core/src/main/java/com/linecorp/armeria/server/VirtualHostContextPathServicesBuilder.java (1)
  • UnstableApi (44-136)
core/src/main/java/com/linecorp/armeria/server/ContextPathServiceBindingBuilder.java (1)
  • UnstableApi (36-44)
⏰ Context from checks skipped due to timeout of 90000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (12)
  • GitHub Check: build-ubicloud-standard-16-jdk-17-min-java-17-coverage
  • GitHub Check: build-ubicloud-standard-16-jdk-8
  • GitHub Check: build-ubicloud-standard-16-jdk-21-snapshot-blockhound
  • GitHub Check: build-ubicloud-standard-16-jdk-11
  • GitHub Check: build-ubicloud-standard-16-jdk-17-leak
  • GitHub Check: build-ubicloud-standard-16-jdk-17-min-java-11
  • GitHub Check: build-windows-latest-jdk-21
  • GitHub Check: build-macos-latest-jdk-21
  • GitHub Check: flaky-tests
  • GitHub Check: site
  • GitHub Check: lint
  • GitHub Check: Kubernetes Chaos test
🔇 Additional comments (8)
core/src/test/java/com/linecorp/armeria/server/HttpServerNestedContextPathTest.java (2)

577-590: LGTM!

The helper class is clean and serves its purpose well for pairing expected status codes with response bodies.


52-154: Solid test configuration covering the nested context path API.

The server configuration exercises a comprehensive range of scenarios: multi-path context sets, deeply nested contexts (4 levels with /single-path1/.../single-path4), virtual hosts, exact vs. prefix matching, and parameterized routes. This provides good coverage for the new feature.

core/src/main/java/com/linecorp/armeria/server/ContextPathServicesBuilder.java (1)

25-29: Concrete nested contextPath implementation validates inputs and composes paths correctly

The contextPath(Iterable<String>, Consumer<ContextPathServicesBuilder>) and single-path overload both:

  • Enforce non-null arguments.
  • Validate each path as absolute via RouteUtil.ensureAbsolutePath.
  • Create a child builder using parent(), virtualHostBuilder(), and mergedContextPaths(paths), then scope the customizer to that child while returning this for fluent chaining.

This matches the abstract contract and Armeria’s “validate on setter” guideline, and nicely centralizes the multi-path-to-single-path delegation via ImmutableSet.of(path).

Also applies to: 106-131

core/src/main/java/com/linecorp/armeria/server/ServerBuilder.java (1)

69-69: Server-level contextPath customizer overloads correctly enforce non-empty, absolute paths

The new contextPath(Iterable<String>, Consumer<ContextPathServicesBuilder>) and single-path overload:

  • Enforce non-null contextPaths and customizer.
  • Explicitly reject empty iterables with a clear IllegalArgumentException, satisfying the “must be non-empty” contract.
  • Validate each context path as absolute via RouteUtil.ensureAbsolutePath.
  • Reuse the existing contextPath(Iterable<String>) factory so builder creation stays centralized, then scope the customizer to that scoped builder.

This design lines up well with the dev-guide recommendation to validate arguments at the point they’re set and gives a clean, Java-friendly nested DSL for context paths.

Also applies to: 103-103, 1350-1395

core/src/main/java/com/linecorp/armeria/server/VirtualHostContextPathServicesBuilder.java (1)

25-29: Virtual-host nested contextPath APIs mirror server-side behavior and validate paths eagerly

The new contextPath(Iterable<String>, Consumer<VirtualHostContextPathServicesBuilder>) and single-path overload:

  • Null-check both arguments.
  • Ensure each child path is absolute via RouteUtil.ensureAbsolutePath before computing merged context paths.
  • Build a child VirtualHostContextPathServicesBuilder with parent(), virtualHostBuilder(), and mergedContextPaths(paths), then apply the customizer and return this.

This matches the semantics of the server-level ContextPathServicesBuilder and keeps validation and path composition consistent across entry points.

Also applies to: 108-135

kotlin/src/test/kotlin/com/linecorp/armeria/server/kotlin/HttpServerNestedContextPathTest.kt (3)

49-187: Comprehensive server and virtual-host setup exercises the full nested context-path surface

The ServerExtension configuration plus the TEST_URLS / VIRTUAL_HOST_TEST_URLS tables do a nice job of:

  • Covering multi-depth Cartesian products of context paths under a base context path.
  • Validating both annotated services and plain service/serviceUnder routes, including prefix and exact-match behavior.
  • Exercising both the default virtual host and a named virtual host (foo.com), which is important for the VirtualHostBuilder-side APIs.

This gives strong end-to-end coverage for the new nested contextPath DSL.

Also applies to: 190-378


386-560: Validation and normalization tests align tightly with the new contextPath contract

The tests for:

  • Empty and relative (top-level and nested) context paths.
  • Duplicate context paths being merged without generating extra distinct routes.
  • No double-slash in exact routes for both base and nested context paths.

all directly exercise the new Java-side validation and merging logic and will catch regressions in path validation or normalization. The use of RoutePathType.EXACT and route enumeration to assert on actual registered paths is particularly helpful for guarding against subtle path-joining bugs.


562-668: Duplicate-route tests ensure nested context paths respect RejectedRouteHandler semantics

The four duplicate-route tests (for both ServerBuilder and VirtualHostBuilder, at 1- and 2-depth nesting):

  • Configure RejectedRouteHandler.FAIL.
  • Intentionally register identical effective routes via different nested-context combinations.
  • Assert that DuplicateRouteException is thrown on build().

These are important to verify that the nested contextPath APIs integrate correctly with the existing duplicate-route detection logic and will help prevent subtle regressions in future refactors.

Comment on lines +426 to +427
@Test
void duplicatedContextPathShouldbeMerged() {
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟡 Minor

Minor typo in method name.

The method name has inconsistent capitalization: Shouldbe should be ShouldBe.

-    void duplicatedContextPathShouldbeMerged() {
+    void duplicatedContextPathShouldBeMerged() {
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
@Test
void duplicatedContextPathShouldbeMerged() {
@Test
void duplicatedContextPathShouldBeMerged() {
🤖 Prompt for AI Agents
In
core/src/test/java/com/linecorp/armeria/server/HttpServerNestedContextPathTest.java
around lines 426 to 427, the test method name has a capitalization typo:
`duplicatedContextPathShouldbeMerged` should be
`duplicatedContextPathShouldBeMerged`; rename the method to correct the
camel-casing (update declaration and any references) so `Shouldbe` becomes
`ShouldBe`.

@chickenchickenlove
Copy link
Copy Markdown
Contributor Author

@ikhoon nim,
I made a new commit based on your comments 😄
When you have time, take a look!

@chickenchickenlove
Copy link
Copy Markdown
Contributor Author

Call for review @jrhee17 @minwoox @trustin .
When you have time, please take a look. 🙇‍♂️

requireNonNull(paths, "contextPaths");
requireNonNull(customizer, "customizer");

for (String path : paths) {
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Question) Should the empty check be also done here? (if paths.isEmpty())

requireNonNull(paths, "contextPaths");
requireNonNull(customizer, "customizer");

for (String path : paths) {
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Question) Is there no need to check if paths is empty here?

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

Thanks for your comments!
I added empty check logic in both ContextPathServercesBuilder and VirutlaHostContextPathServiceBuilder.!

@minwoox minwoox added this to the 1.35.0 milestone Dec 9, 2025
Copy link
Copy Markdown
Contributor

@minwoox minwoox left a comment

Choose a reason for hiding this comment

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

Thanks! 😄

}

VirtualHostBuilder virtualHostBuilder() {
return this.virtualHostBuilder;
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

nit:

Suggested change
return this.virtualHostBuilder;
return virtualHostBuilder;

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

Addressed!

Copy link
Copy Markdown
Contributor

@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: 0

🧹 Nitpick comments (2)
core/src/main/java/com/linecorp/armeria/server/AbstractContextPathServicesBuilder.java (1)

423-432: Consider validating that paths is not empty in mergedContextPaths.

The mergedContextPaths method silently returns an empty set if paths is empty, which could lead to confusing behavior. While the calling code in subclasses validates this, defensive validation here would catch misuse from future callers.

However, since the abstract contextPath methods document that paths must be non-empty and the concrete implementations validate this before calling mergedContextPaths, this is acceptable.

 final Set<String> mergedContextPaths(Iterable<String> paths) {
+    requireNonNull(paths, "paths");
     final Set<String> mergedContextPaths = new HashSet<>();
     for (String currentContextPath : contextPaths()) {
         for (String childContextPath : paths) {
             final String mergedContextPath = currentContextPath + childContextPath;
             mergedContextPaths.add(mergedContextPath);
         }
     }
     return ImmutableSet.copyOf(mergedContextPaths);
 }
core/src/main/java/com/linecorp/armeria/server/VirtualHostContextPathServicesBuilder.java (1)

133-141: Consider extracting the common validation logic to reduce duplication.

Both ContextPathServicesBuilder and VirtualHostContextPathServicesBuilder have identical validation logic in their contextPath(Iterable, Consumer) implementations. While the child builder instantiation must differ, the validation could potentially be extracted to a helper in AbstractContextPathServicesBuilder.

For example, add a protected helper in AbstractContextPathServicesBuilder:

protected static void validateContextPaths(Iterable<String> paths) {
    requireNonNull(paths, "contextPaths");
    boolean isEmpty = true;
    for (String path : paths) {
        isEmpty = false;
        RouteUtil.ensureAbsolutePath(path, "contextPath");
    }
    if (isEmpty) {
        throw new IllegalArgumentException("contextPaths must not be empty.");
    }
}

Then in subclasses:

 public VirtualHostContextPathServicesBuilder contextPath(
         Iterable<String> paths,
         Consumer<VirtualHostContextPathServicesBuilder> customizer) {
-    requireNonNull(paths, "contextPaths");
     requireNonNull(customizer, "customizer");
-
-    boolean isEmpty = true;
-    for (String path : paths) {
-        isEmpty = false;
-        RouteUtil.ensureAbsolutePath(path, "contextPath");
-    }
-
-    if (isEmpty) {
-        throw new IllegalArgumentException("contextPaths must not be empty.");
-    }
+    validateContextPaths(paths);
     
     final VirtualHostContextPathServicesBuilder child = ...
📜 Review details

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 0126a4d and b877676.

📒 Files selected for processing (3)
  • core/src/main/java/com/linecorp/armeria/server/AbstractContextPathServicesBuilder.java (2 hunks)
  • core/src/main/java/com/linecorp/armeria/server/ContextPathServicesBuilder.java (2 hunks)
  • core/src/main/java/com/linecorp/armeria/server/VirtualHostContextPathServicesBuilder.java (2 hunks)
🧰 Additional context used
📓 Path-based instructions (1)
**/*.java

⚙️ CodeRabbit configuration file

**/*.java: - The primary coding conventions and style guide for this project are defined in site/src/pages/community/developer-guide.mdx. Please strictly adhere to this file as the ultimate source of truth for all style and convention-related feedback.

2. Specific check for @UnstableApi

  • Review all newly added public classes and methods to ensure they have the @UnstableApi annotation.
  • However, this annotation is NOT required under the following conditions:
    • If the class or method is located in a package containing .internal or .testing.
    • If the class or method is located in a test source set.
    • If a public method is part of a class that is already annotated with @UnstableApi.

Files:

  • core/src/main/java/com/linecorp/armeria/server/VirtualHostContextPathServicesBuilder.java
  • core/src/main/java/com/linecorp/armeria/server/ContextPathServicesBuilder.java
  • core/src/main/java/com/linecorp/armeria/server/AbstractContextPathServicesBuilder.java
🧬 Code graph analysis (1)
core/src/main/java/com/linecorp/armeria/server/VirtualHostContextPathServicesBuilder.java (1)
core/src/main/java/com/linecorp/armeria/server/ContextPathServicesBuilder.java (1)
  • UnstableApi (43-139)
⏰ Context from checks skipped due to timeout of 90000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (13)
  • GitHub Check: Kubernetes Chaos test
  • GitHub Check: build-ubicloud-standard-16-jdk-8
  • GitHub Check: build-ubicloud-standard-16-jdk-25
  • GitHub Check: build-ubicloud-standard-16-jdk-17-min-java-17-coverage
  • GitHub Check: build-ubicloud-standard-16-jdk-17-leak
  • GitHub Check: build-ubicloud-standard-16-jdk-17-min-java-11
  • GitHub Check: build-ubicloud-standard-16-jdk-21-snapshot-blockhound
  • GitHub Check: build-ubicloud-standard-16-jdk-11
  • GitHub Check: build-windows-latest-jdk-25
  • GitHub Check: site
  • GitHub Check: lint
  • GitHub Check: build-macos-latest-jdk-25
  • GitHub Check: flaky-tests
🔇 Additional comments (5)
core/src/main/java/com/linecorp/armeria/server/AbstractContextPathServicesBuilder.java (2)

414-421: LGTM! Package-private accessors for child builder construction.

These accessors enable subclasses to create child builders while maintaining encapsulation. The visibility is appropriately restricted.


434-469: Well-documented abstract API for nested context paths.

The JavaDoc clearly explains the Cartesian product semantics, the absolute path requirement, and provides a helpful example. The @throws and @see annotations improve discoverability.

core/src/main/java/com/linecorp/armeria/server/ContextPathServicesBuilder.java (2)

107-130: Implementation correctly validates inputs and creates nested context.

The validation logic properly:

  1. Checks non-null for both arguments
  2. Validates each path is absolute via RouteUtil.ensureAbsolutePath
  3. Tracks and throws on empty paths

This addresses the past review comment from jrhee17 about empty check.


132-138: LGTM! Clean delegation to the Iterable overload.

The single-path convenience method correctly delegates to the multi-path variant using ImmutableSet.of(path).

core/src/main/java/com/linecorp/armeria/server/VirtualHostContextPathServicesBuilder.java (1)

108-131: Implementation mirrors ContextPathServicesBuilder correctly.

The validation and child-builder creation logic is identical to ContextPathServicesBuilder, which is expected given the abstract API contract. The duplication is unavoidable due to Java's generics requiring concrete types for child builder instantiation.

One minor observation: this addresses the past review comment from jrhee17 regarding the empty check.

Copy link
Copy Markdown
Contributor

@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: 0

🧹 Nitpick comments (1)
core/src/main/java/com/linecorp/armeria/server/VirtualHostContextPathServicesBuilder.java (1)

108-131: Implementation is correct and consistent with ContextPathServicesBuilder.

The validation and child builder creation logic mirrors the implementation in ContextPathServicesBuilder, addressing jrhee17's previous question about empty path validation.

Consider extracting the shared validation logic (null checks, empty check, absolute path validation) into a helper method in AbstractContextPathServicesBuilder to reduce duplication between this class and ContextPathServicesBuilder.

// In AbstractContextPathServicesBuilder, add a protected helper:
protected static void validateContextPaths(Iterable<String> paths) {
    boolean isEmpty = true;
    for (String path : paths) {
        isEmpty = false;
        RouteUtil.ensureAbsolutePath(path, "contextPath");
    }
    if (isEmpty) {
        throw new IllegalArgumentException("contextPaths must not be empty.");
    }
}
📜 Review details

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between b877676 and ef78294.

📒 Files selected for processing (2)
  • core/src/main/java/com/linecorp/armeria/server/ContextPathServicesBuilder.java (2 hunks)
  • core/src/main/java/com/linecorp/armeria/server/VirtualHostContextPathServicesBuilder.java (2 hunks)
🧰 Additional context used
📓 Path-based instructions (1)
**/*.java

⚙️ CodeRabbit configuration file

**/*.java: - The primary coding conventions and style guide for this project are defined in site/src/pages/community/developer-guide.mdx. Please strictly adhere to this file as the ultimate source of truth for all style and convention-related feedback.

2. Specific check for @UnstableApi

  • Review all newly added public classes and methods to ensure they have the @UnstableApi annotation.
  • However, this annotation is NOT required under the following conditions:
    • If the class or method is located in a package containing .internal or .testing.
    • If the class or method is located in a test source set.
    • If a public method is part of a class that is already annotated with @UnstableApi.

Files:

  • core/src/main/java/com/linecorp/armeria/server/ContextPathServicesBuilder.java
  • core/src/main/java/com/linecorp/armeria/server/VirtualHostContextPathServicesBuilder.java
🧬 Code graph analysis (1)
core/src/main/java/com/linecorp/armeria/server/ContextPathServicesBuilder.java (1)
core/src/main/java/com/linecorp/armeria/server/VirtualHostContextPathServicesBuilder.java (1)
  • UnstableApi (44-142)
⏰ Context from checks skipped due to timeout of 90000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (13)
  • GitHub Check: build-ubicloud-standard-16-jdk-17-min-java-11
  • GitHub Check: build-ubicloud-standard-16-jdk-8
  • GitHub Check: build-ubicloud-standard-16-jdk-11
  • GitHub Check: build-ubicloud-standard-16-jdk-21-snapshot-blockhound
  • GitHub Check: build-ubicloud-standard-16-jdk-25
  • GitHub Check: build-macos-latest-jdk-25
  • GitHub Check: build-ubicloud-standard-16-jdk-17-min-java-17-coverage
  • GitHub Check: build-ubicloud-standard-16-jdk-17-leak
  • GitHub Check: build-windows-latest-jdk-25
  • GitHub Check: site
  • GitHub Check: lint
  • GitHub Check: flaky-tests
  • GitHub Check: Kubernetes Chaos test
🔇 Additional comments (5)
core/src/main/java/com/linecorp/armeria/server/ContextPathServicesBuilder.java (3)

25-28: LGTM!

The imports for ImmutableSet and RouteUtil are appropriate for the new nested context path functionality.


107-130: Implementation correctly addresses previous review concerns.

The validation logic properly handles:

  • Null checks for both paths and customizer
  • Empty iterable detection via the isEmpty flag pattern (since Iterable lacks isEmpty())
  • Absolute path validation via RouteUtil.ensureAbsolutePath

This addresses jrhee17's previous question about empty path validation.


132-138: LGTM!

The single-path overload correctly delegates to the iterable variant using ImmutableSet.of(path), as suggested in previous reviews.

core/src/main/java/com/linecorp/armeria/server/VirtualHostContextPathServicesBuilder.java (2)

25-28: LGTM!

Imports are consistent with ContextPathServicesBuilder.


133-141: LGTM!

The single-path overload correctly delegates to the iterable variant, consistent with the pattern in ContextPathServicesBuilder.

@minwoox minwoox merged commit b043f96 into line:main Dec 31, 2025
17 of 18 checks passed
@minwoox
Copy link
Copy Markdown
Contributor

minwoox commented Dec 31, 2025

Thanks, @chickenchickenlove!

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

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Nested context paths

4 participants