New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Provide a way to set the root context path #4802
Conversation
c3d9c46
to
abe193b
Compare
core/src/test/java/com/linecorp/armeria/server/ServerBuilderTest.java
Outdated
Show resolved
Hide resolved
Had an internal discussion about the API design of this PR. We were worried that the server and service builder become too complex by adding It should be nicer to reuse the existing API to build the services rather than reinventing the wheel. On second thought, I think, we can add a new concept of virtual hosting that serves all services under a certain path like For example: Server
.builder()
// return a path-based `VirtualHost` prefixed with `/api/v0`
.virtualHostWithPath("/api/v0")
.service(...)
.decorator(...)
.and()
// return a path-based `VirtualHost` prefixed with `/api/v1`
.virtualHostWithPath("/api/v1")
.service(...)
.decorator(...)
.and() To implement a path-based virtual host, you may want to check out the following points.
|
Thanks for the detailed explanation. let me update this draft PR soon. 🙇 |
dffeac3
to
fd58e5f
Compare
db59dcf
to
9fb9f74
Compare
core/src/main/java/com/linecorp/armeria/server/VirtualHostBuilder.java
Outdated
Show resolved
Hide resolved
core/src/test/java/com/linecorp/armeria/server/ServerBuilderTest.java
Outdated
Show resolved
Hide resolved
core/src/test/java/com/linecorp/armeria/server/ServerBuilderTest.java
Outdated
Show resolved
Hide resolved
core/src/main/java/com/linecorp/armeria/server/VirtualHostBuilder.java
Outdated
Show resolved
Hide resolved
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Left some minor comments, but I think the overall direction looks good and the PR should be almost done soon.
I think if this is handled quickly, hopefully it can be released in this iteration 🤞
@@ -77,6 +77,8 @@ public final class VirtualHost { | |||
private final String defaultHostname; | |||
private final String hostnamePattern; | |||
private final int port; | |||
private final String contextPath; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I don't think we need to add contextPath
to VirtualHost
for the new direction.
What do you think of reverting this file?
@@ -181,6 +184,12 @@ static String normalizeDefaultHostname(String defaultHostname) { | |||
return Ascii.toLowerCase(defaultHostname); | |||
} | |||
|
|||
static String normalizeContextPath(String contextPath) { | |||
requireNonNull(contextPath, "contextPath"); | |||
// TODO Implement this |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I think we can just do something like the following.
RouteUtil.ensureAbsolutePath(contextPath, "contextPath");
We probably don't even need to define a dedicated method for this
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
aha, thank you. let me address it.
core/src/main/java/com/linecorp/armeria/server/VirtualHostBuilder.java
Outdated
Show resolved
Hide resolved
@@ -478,7 +501,8 @@ public VirtualHostBuilder serviceUnder(String pathPrefix, HttpService service) { | |||
* @throws IllegalArgumentException if the specified path pattern is invalid | |||
*/ | |||
public VirtualHostBuilder service(String pathPattern, HttpService service) { | |||
service(Route.builder().path(pathPattern).build(), service); | |||
final String prefixed = contextPath == null ? pathPattern : contextPath + pathPattern; | |||
service(Route.builder().path(prefixed).build(), service); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Instead of adding the prefix here, what do you think of adding the prefix at VirtualHostBuilder#build
?
If we add routes here there are several problems:
- Some builders like
VirtualHostBuilder#route()
, orVirtualHostBuilder#annotatedService
,VirtualHostBuilder#serviceUnder
won't be applied - If the
contextPath
value changes, then the applied prefix can be inconsistent
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
thanks. understood!
@@ -677,9 +701,29 @@ private List<ServiceConfigSetters> getServiceConfigSetters( | |||
} else { | |||
serviceConfigSetters = ImmutableList.copyOf(this.serviceConfigSetters); | |||
} | |||
|
|||
if (defaultVirtualHostBuilder.contextPath() != null) { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Ditto, I propose that we add prefixes at VirtualHostBuilder#build
.
In detail, I propose the following change so that ServiceConfigBuilder
accepts the prefix before creating the actual ServiceConfig
.
+++ b/core/src/main/java/com/linecorp/armeria/server/ServiceConfigBuilder.java
@@ -293,7 +293,8 @@ final class ServiceConfigBuilder implements ServiceConfigSetters {
HttpHeaders virtualHostDefaultHeaders,
Function<? super RoutingContext, ? extends RequestId> defaultRequestIdGenerator,
ServiceErrorHandler defaultServiceErrorHandler,
- @Nullable UnhandledExceptionsReporter unhandledExceptionsReporter) {
+ @Nullable UnhandledExceptionsReporter unhandledExceptionsReporter,
+ String contextPath) {
ServiceErrorHandler errorHandler =
serviceErrorHandler != null ? serviceErrorHandler.orElse(defaultServiceErrorHandler)
: defaultServiceErrorHandler;
@@ -303,7 +304,7 @@ final class ServiceConfigBuilder implements ServiceConfigSetters {
}
return new ServiceConfig(
- route, mappedRoute == null ? route : mappedRoute,
+ route.withPrefix(contextPath), mappedRoute == null ? route : mappedRoute,
This can be passed from VirtualHostBuilder
like the following:
+++ b/core/src/main/java/com/linecorp/armeria/server/VirtualHostBuilder.java
@@ -1338,7 +1337,7 @@ public final class VirtualHostBuilder implements TlsSetters {
successFunction, requestAutoAbortDelayMillis,
multipartUploadsLocation, defaultHeaders,
requestIdGenerator, defaultErrorHandler,
- unhandledExceptionsReporter);
+ unhandledExceptionsReporter, contextPath);
}).collect(toImmutableList());
final ServiceConfig fallbackServiceConfig =
@@ -1347,7 +1346,7 @@ public final class VirtualHostBuilder implements TlsSetters {
accessLogWriter, blockingTaskExecutor, successFunction,
requestAutoAbortDelayMillis, multipartUploadsLocation,
defaultHeaders, requestIdGenerator,
- defaultErrorHandler, unhandledExceptionsReporter);
+ defaultErrorHandler, unhandledExceptionsReporter, "/");
Note that fallbackServiceConfig
should still catch all requests, so the prefix isn't applied at line 1346.
@@ -320,6 +320,10 @@ ServiceConfig build(ServiceNaming defaultServiceNaming, | |||
requestIdGenerator != null ? requestIdGenerator : defaultRequestIdGenerator, errorHandler); | |||
} | |||
|
|||
public ServiceConfigBuilder getPrefixedServiceConfigBuilder(String contextPath) { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I don't think this method is necessary if we can set the contextPath
when ServiceConfigBuilder#build
is invoked.
core/src/main/java/com/linecorp/armeria/server/VirtualHostBuilder.java
Outdated
Show resolved
Hide resolved
@@ -122,6 +123,8 @@ public final class VirtualHostBuilder implements TlsSetters { | |||
private String hostnamePattern; | |||
private int port = -1; | |||
@Nullable | |||
private String contextPath; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Can we just set to /
by default and remove the @Nullable
contract?
@Test | ||
void virtualHostContextPath() { | ||
final Server server = Server | ||
.builder() |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I think we should also consider decorators:
Server.builder()
.decorator("/asdf", (delegate, ctx, req) -> HttpResponse.of(200))
...
We probably need to apply the contextPath
like the following:
@@ -737,7 +737,7 @@ public final class VirtualHostBuilder implements TlsSetters {
@Nullable
private Function<? super HttpService, ? extends HttpService> getRouteDecoratingService(
- @Nullable VirtualHostBuilder defaultVirtualHostBuilder) {
+ @Nullable VirtualHostBuilder defaultVirtualHostBuilder, String contextPath) {
final List<RouteDecoratingService> routeDecoratingServices;
if (defaultVirtualHostBuilder != null) {
routeDecoratingServices = ImmutableList.<RouteDecoratingService>builder()
@@ -749,8 +749,11 @@ public final class VirtualHostBuilder implements TlsSetters {
}
if (!routeDecoratingServices.isEmpty()) {
- return RouteDecoratingService.newDecorator(
- Routers.ofRouteDecoratingService(routeDecoratingServices));
+ final List<RouteDecoratingService> prefixed =
+ routeDecoratingServices.stream()
+ .map(service -> service.withRoutePrefix(contextPath))
+ .collect(Collectors.toList());
+ return RouteDecoratingService.newDecorator(Routers.ofRouteDecoratingService(prefixed));
} else {
return null;
}
where the routes can be overridden using delegation
+++ b/core/src/main/java/com/linecorp/armeria/internal/server/RouteDecoratingService.java
@@ -91,6 +91,15 @@ public final class RouteDecoratingService implements HttpService {
decorator = requireNonNull(decoratorFunction, "decoratorFunction").apply(this);
}
+ public RouteDecoratingService(Route route, HttpService decorator) {
+ this.route = requireNonNull(route, "route");
+ this.decorator = requireNonNull(decorator, "decorator");
+ }
+
+ public RouteDecoratingService withRoutePrefix(String prefix) {
+ return new RouteDecoratingService(route.withPrefix(prefix), decorator);
+ }
+
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Ah... decorator. thanks.
core/src/main/java/com/linecorp/armeria/server/VirtualHostBuilder.java
Outdated
Show resolved
Hide resolved
core/src/test/java/com/linecorp/armeria/server/ServerBuilderTest.java
Outdated
Show resolved
Hide resolved
@jrhee17 nim, Thanks for the review. I'll address your comments. |
- address comments
- consider decorator - rename API to baseContextPath - handle VirtualHost separatly - add prefixes at VirtualHostBuilder#build.
9fb9f74
to
f452ee4
Compare
f452ee4
to
49f37e7
Compare
core/src/test/java/com/linecorp/armeria/server/RouteDecoratingServiceTest.java
Outdated
Show resolved
Hide resolved
core/src/test/java/com/linecorp/armeria/server/RouteDecoratingServiceTest.java
Outdated
Show resolved
Hide resolved
d6d4c51
to
d41dc30
Compare
core/src/test/java/com/linecorp/armeria/server/VirtualHostBuilderTest.java
Outdated
Show resolved
Hide resolved
Codecov ReportAll modified lines are covered by tests ✅
Additional details and impacted files@@ Coverage Diff @@
## main #4802 +/- ##
============================================
+ Coverage 74.16% 74.17% +0.01%
- Complexity 19891 19895 +4
============================================
Files 1707 1707
Lines 73325 73338 +13
Branches 9364 9364
============================================
+ Hits 54381 54398 +17
+ Misses 14511 14507 -4
Partials 4433 4433
☔ View full report in Codecov by Sentry. |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Looks almost done! Only minor comments for me 🙇
@@ -335,7 +336,7 @@ ServiceConfig build(ServiceNaming defaultServiceNaming, | |||
} | |||
|
|||
return new ServiceConfig( | |||
route, mappedRoute == null ? route : mappedRoute, | |||
route.withPrefix(contextPath), mappedRoute == null ? route : mappedRoute, |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Following the contract of mappedRoute
:
armeria/core/src/main/java/com/linecorp/armeria/server/ServiceConfig.java
Lines 250 to 251 in 5f9c1b6
* If the service is not an {@link HttpServiceWithRoutes}, the {@link Route} is the same as | |
* {@link #route()}. |
We can probably also dedup the route.withPrefix(contextPath)
route.withPrefix(contextPath), mappedRoute == null ? route : mappedRoute, | |
route.withPrefix(contextPath), mappedRoute == null ? route.withPrefix(contextPath) : mappedRoute, |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
core/src/main/java/com/linecorp/armeria/server/VirtualHostBuilder.java
Outdated
Show resolved
Hide resolved
core/src/main/java/com/linecorp/armeria/internal/server/RouteDecoratingService.java
Outdated
Show resolved
Hide resolved
core/src/main/java/com/linecorp/armeria/server/VirtualHostBuilder.java
Outdated
Show resolved
Hide resolved
core/src/main/java/com/linecorp/armeria/server/ServerBuilder.java
Outdated
Show resolved
Hide resolved
core/src/test/java/com/linecorp/armeria/server/VirtualHostBuilderTest.java
Outdated
Show resolved
Hide resolved
final Server server = Server | ||
.builder() | ||
// 1 | ||
.port(8888, SessionProtocol.HTTP) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Setting a fixed port can be a cause of flakiness. Can you modify so that the port is set as random (same as the other tests in this class?)
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Sorry, Is there some way to test virtualHost with port (e.g. .virtualHost("*.foo.com:8888")
) without setting a fixed port? 😢
When I set the port as 0 ( to use random port) I've got the following error.
virtual host port: 8888 (expected: one of [])
java.lang.IllegalStateException: virtual host port: 8888 (expected: one of [])
at com.google.common.base.Preconditions.checkState(Preconditions.java:715)
at com.linecorp.armeria.server.ServerBuilder.buildServerConfig(ServerBuilder.java:2056)
at com.linecorp.armeria.server.ServerBuilder.build(ServerBuilder.java:2002)
at com.linecorp.armeria.server.ServerBuilderTest.baseContextPathApplied(ServerBuilderTest.java:740)
at java.lang.reflect.Method.invoke(Method.java:498)
So I guessed I should set a fixed port to test it.
I've investigated test codes in this class, there's a similar test to follow.
.virtualHost("*.example2.com:8080") |
and it's using http()
.http(8080) |
Did you mean to set the port using .http()
.. ?.?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I see 😅 If you would like to test virtual hosts with ports, I think the following reference can help:
armeria/core/src/test/java/com/linecorp/armeria/server/PortBasedVirtualHostTest.java
Lines 48 to 56 in b604f3f
try (ServerSocket ss = new ServerSocket(0)) { | |
normalServerPort = ss.getLocalPort(); | |
} | |
try (ServerSocket ss = new ServerSocket(0)) { | |
virtualHostPort = ss.getLocalPort(); | |
} | |
try (ServerSocket ss = new ServerSocket(0)) { | |
fooHostPort = ss.getLocalPort(); | |
} |
Optionally since the test class is convoluted (so it is difficult to use ServerExtension
), I wouldn't be against just creating a separate test class (either way is fine)
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
thanks! let me handle it.
Co-authored-by: jrhee17 <guins_j@guins.org>
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Still looks good 👍 I think this PR is ready to go once my comment on testing is resolved
final Server server = Server | ||
.builder() | ||
// 1 | ||
.port(8888, SessionProtocol.HTTP) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I see 😅 If you would like to test virtual hosts with ports, I think the following reference can help:
armeria/core/src/test/java/com/linecorp/armeria/server/PortBasedVirtualHostTest.java
Lines 48 to 56 in b604f3f
try (ServerSocket ss = new ServerSocket(0)) { | |
normalServerPort = ss.getLocalPort(); | |
} | |
try (ServerSocket ss = new ServerSocket(0)) { | |
virtualHostPort = ss.getLocalPort(); | |
} | |
try (ServerSocket ss = new ServerSocket(0)) { | |
fooHostPort = ss.getLocalPort(); | |
} |
Optionally since the test class is convoluted (so it is difficult to use ServerExtension
), I wouldn't be against just creating a separate test class (either way is fine)
- seperate test class
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Changes look good to me! Thanks for your patience @bkkanq 🙇 🚀 🙇
Note that I've pushed a small commit which renames contextPath
-> baseContextPath
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
@@ -335,7 +336,8 @@ ServiceConfig build(ServiceNaming defaultServiceNaming, | |||
} | |||
|
|||
return new ServiceConfig( | |||
route, mappedRoute == null ? route : mappedRoute, | |||
route.withPrefix(baseContextPath), |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
nit; we can extract the route:
final Route withPrefix = route.withPrefix(baseContextPath);
return new ServiceConfig(
withPrefix, mappedRoute == null ? withPrefix : mappedRoute,
Routers.ofRouteDecoratingService(routeDecoratingServices)); | ||
final List<RouteDecoratingService> prefixed = routeDecoratingServices.stream() | ||
.map(service -> service.withRoutePrefix(baseContextPath)) | ||
.collect(Collectors.toList()); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
We usually use:
.collect(Collectors.toList()); | |
.collect(toImmutableList()); |
import com.linecorp.armeria.internal.testing.MockAddressResolverGroup; | ||
import com.linecorp.armeria.testing.junit5.server.ServerExtension; | ||
|
||
public class BaseContextPathTest { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
We can remove public
modifiers in this class.
Let's also add @FlakyTest
because this can be flaky.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Motivation: Described in line#3591 Modifications: - Add `ContextServiceBuilder` to bind a root context path to `VirtualHost` Result: - User can set the root context path - TODO: Support to set `contextPath` in Armeria Spring integration - Closes line#3591 Co-authored-by: jrhee17 <guins_j@guins.org> Co-authored-by: Ikhun Um <ih.pert@gmail.com>
Motivation:
Described in #3591
Modifications:
ContextServiceBuilder
to bind a root context path toVirtualHost
Result:
contextPath
in Armeria Spring integration