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
experimental doc service route guessing #4987
experimental doc service route guessing #4987
Conversation
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.
This looks great. Thanks a lot! 😄
@@ -149,6 +152,11 @@ public ServiceSpecification(Iterable<ServiceInfo> services, | |||
this.exampleHeaders = ImmutableList.copyOf(requireNonNull(exampleHeaders, "exampleHeaders")); | |||
} | |||
|
|||
public ServiceSpecification withServiceRoute(Route serviceRoute) { | |||
this.serviceRoute = serviceRoute; |
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.
Global suggestion:
How about renaming serviceRoute
to docServiceRoute
which is more specific?
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.
Would it make sense to set this route only when Route.pathType()
is RoutePathType.PREFIX
?
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.
What about EXACT
?
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 am adding that as default too, seems like it shouldn't hurt.
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't serve docserivce using EXACT
so I think only one of PREFIX
, REGEX
and GLOB
can be used.
If REGEX
and GLOB
are used, I thought we could just don't set it because we can't handle it. 😉
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.
Makes sense, even if we can handle it seems like not worth it. Why would somebody mount DocService
under a glob or regex path? 😅
About exact, this is related to how client code is generated, right? Because we are not serving a single static page but rather a page under path /docs/#/...
?
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.
Makes sense, even if we can handle it seems like not worth it. Why would somebody mount DocService under a glob or regex path?
That is correct. 😄
Because we are not serving a single static page but rather a page under path /docs/#/...?
That is also correct. The client sends additional requests whose path is:
/docs/main.js?somehash
/docs/specification.json
/docs/versions.json
/docs/injected.js
/docs/schemas.json
/docs/assets/favicon.png
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.
@minwoox Please take a second look when you are free 😄
Also, what is the standard procedure for testing those changes?
I am thinking about running Annotated
, GraphQL
, Grpc
doc services and test everything 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.
We don't have automated tests for UI. 😓
We definitely need to set up front-end tests.
core/src/test/java/com/linecorp/armeria/server/docs/ServiceSpecificationRouteTest.java
Show resolved
Hide resolved
core/src/test/java/com/linecorp/armeria/server/docs/ServiceSpecificationRouteTest.java
Outdated
Show resolved
Hide resolved
@@ -203,6 +216,10 @@ const DebugPage: React.FunctionComponent<Props> = ({ | |||
)?.pathMapping || ''; | |||
} | |||
|
|||
if (urlPath.startsWith('/')) { |
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.
Question: Is there a case that urlPath
doesn't start with a slash?
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.
Hmm I was thinking about exact:
and relative path (if possible?)
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 exact:
will be stripped by the extractUrlPath
method above but I'm not sure about the relative path.
So let's leave it as is. We can fix it later if we found any problems. 😉
/** | ||
* Return a {@link ServiceSpecification} that contains the {@link Route} info of | ||
* the attached {@link DocService}. | ||
*/ | ||
public ServiceSpecification withDocServiceRoute(Route docServiceRoute) { | ||
this.docServiceRoute = docServiceRoute; | ||
return 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.
Can we keep this class immutable? It would also be nice if this method is not public.
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.
Got it, I think it makes sense to move this route to exampleSupport
because it contains path information as well? What do you think, I am not sure about creating a new class just to put a single method 😆
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.
That's a good idea! We probably should rename it though. What do you think about SpecificationSupport
?
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.
Never mind. Just realized that you fixed it in a better way.
const parseServerRootPath = (docServiceRoute: string) => { | ||
const plainRoute = docServiceRoute.replace('/*', ''); | ||
const index = window.location.href.indexOf(plainRoute); | ||
|
||
if (index === -1) { | ||
return ''; | ||
} | ||
const rootPath = window.location.href.substring(0, index); | ||
return rootPath; | ||
}; |
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 also show an error message if a user bound a DocService
with other routes than prefix:
? I think we can throw an exception on the server side, then we could simplify the logic here.
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.
Good idea, exact paths don't work (I personally experienced it in the past).
Globs and regex, should we disable them?. I feel like it won't really make sense unless somebody has a particular need (i.e. bound it to a dynamic path).
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 so. Let's allow prefix:
only.
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 👍
Can you also check if exact paths correctly use the base path?
i.e. if the following line is false:
if (!exactPathMapping) { |
import com.linecorp.armeria.server.ServerBuilder; | ||
import com.linecorp.armeria.testing.junit5.server.ServerExtension; | ||
|
||
class ServiceSpecificationRouteTest { |
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.
What do you think of adding another service (maybe in AnnotatedDocServiceTest
) so that we can at least verify manually easily if doc service works correctly behind a proxy?
e.g.
sb.serviceUnder("/proxy", (ctx, req) -> {
final String origPath = req.path();
assertThat(origPath).startsWith("/proxy");
final String newPath = req.path().substring("/proxy".length());
final HttpRequest newReq = req.withHeaders(req.headers().toBuilder().path(newPath).build());
return server.webClient().execute(newReq);
});
and we can call http://localhost:3000/proxy/docs
when testing
@@ -116,6 +117,17 @@ const copyTextToClipboard = (text: string) => { | |||
modal.removeChild(textArea); | |||
}; | |||
|
|||
const parseServerRootPath = (docServiceRoute: string) => { | |||
const plainRoute = docServiceRoute.replace('/*', ''); |
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.
What do you think of skipping unless the route ends with /*
?
I'm thinking of regex cases where the path may look like: a/**/b
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.
In backend we configured it in a way that it only works for PREFIX
paths. If it is something else, docServiceRoute
is going to be empty string. But it makes sense to just have the logic here as a safe-guard.
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.
Updated logic to check route type.
return ''; | ||
} | ||
const rootPath = window.location.href.substring(0, index); | ||
return rootPath; |
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.
Question) I haven't tried, but what do you think of returning a relative path instead of the full location? I feel like this can be a confusing point later if either an absolute path or a relative path can be set to additionalPath
i.e. /foo
instead of http://host/foo
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.
Hmm good idea, I was also thinking about the same thing let me try.
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.
Now using a relative path. Tested manually behind a proxy and a non-proxy.
ditto, I have a 3 day weekend, have some time to finalize my PRs. |
Hey folks, I want to merge this if possible. We can't still use doc service for our staging services in my company so with this change I can promote usage of Doc service 🙂 |
Thanks for the reminder. Will try to get review/get this merged in the next release (1.25.0). |
core/src/test/java/com/linecorp/armeria/internal/server/annotation/AnnotatedDocServiceTest.java
Outdated
Show resolved
Hide resolved
*/ | ||
@JsonProperty | ||
@Nullable | ||
public Route docServiceRoute() { |
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 will always be aware that Route
can be serialized to a json when modifying the class.
What do you think of either:
- Transforming
Route
to a dedicated dto - Adding unit tests on json serialization which ensures that the contract won't change.
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.
Yep, it is a good idea. Let me handle 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.
OK, I found out that in ServiceSpecification
we serialize the following structs to JSON as well:
- HttpHeaders
- MediaType
So I think it is OK as long as I add a custom serializer to it and add a unit test rather than creating a DTO. Because we did not create DTO for any of the above.
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.
Hmm actually it is not really necessary to add a custom serializer either. How about just keeping @JsonProperty
and adding unit tests?
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.
How about just keeping @JsonProperty and adding unit tests?
Sure sounds good 👍
@ParameterizedTest | ||
@CsvSource({ "/proxy", "/proxy2/nested" }) | ||
void rightPathBehindProxy(String proxyPath) throws IOException { | ||
server.server().reconfigure(sb -> sb.serviceUnder(proxyPath, (ctx, req) -> { |
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 prefer that we don't use reconfigure
in tests since this can affect tests that might be added afterwards.
(Usually when adding tests to files like this, I think it is more convenient to check the added services in ServerExtension
instead of having to check each test to see if the server has been configured)
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 it is more convenient to check the added services in ServerExtension
I did not understand this. My intention was to create the endpoint in test and test it afterwards. Kinda like scoping the server extension to single test 😆 So I wanted to make sure behavior is explicit. Any suggestions on how to do it? Maybe I should create different extensions or different test classes but that can be heavyweight.
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.
Anyway, moved proxies to top-level as pre-defined endpoints for simplicity.
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.
If a server.reconfigure()
is called, the original services defined in ServerExtension
are not restored.
Thus, if a test is executed after rightPathBehindProxy
only /proxy2/nested
and \docs
service will be available.
Alternatively, we could enable ServerExtension#runForEachTest
, but it's probably cheaper to just add the service like done in the following commit you added 👍 .
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 good overall 👍
I think I'll have to run the service and check if there are any regressions, but basically I'm happy with the current PR if it wokrs 👍
Were you able to check this comment? It seems like the actual request that is sent is not prefixed 😅 |
Sorry I must have missed that 😓 I'll get into it tomorrow 👍 |
@@ -59,7 +59,7 @@ export default class ThriftTransport extends Transport { | |||
hdrs.set(name, value); | |||
} | |||
|
|||
return fetch(endpoint.pathMapping, { | |||
return fetch(endpointPath || endpoint.pathMapping, { |
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 most logic in DocService
relies on using the endpoint
's path, whereas we are adding a root path unrelated to endpoints.
Instead of reusing additionalPath
(urlPath
), what do you think of just passing the rootPrefix
(docServiceRoute
) all the way down to the transport layer?
i.e.
armeria/docs-client/src/lib/transports/annotated-http.ts
Lines 98 to 99 in a86bcc5
let newPath = | |
endpointPath || endpoint.pathMapping.substring('exact:'.length); |
This way
- We don't have to worry about edge cases depending on the method type.
- We don't have to worry about url paths that are resolved at the last minute.
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 did not understand. Do you think it would be better if we pass rootPrefix
into this method,
armeria/docs-client/src/lib/transports/transport.ts
Lines 27 to 33 in 4bae026
public async send( | |
method: Method, | |
headers: { [name: string]: string }, | |
bodyJson?: string, | |
endpointPath?: string, | |
queries?: string, | |
): Promise<string> { |
And leave endpointPath
unmodified? So doSend
method takes rootPrefix
instead?
I will push a PR regarding this and LMK if I understood this incorrectly.
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.
BTW it seems like this ||
change in thrift should be fine regardless of that because same logic applies to annotated services?
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 will push a PR regarding this and LMK if I understood this incorrectly.
On second thought, perhaps it's better to just add the prefix automatically to the preset UI. (so the original implementation was fine)
This way if the prefix adding behavior is not what the user wants, worst case the user can just manually remove the prefix.
I'll have to look into a better injection point for this behavior during the weekend, but if you have a better idea which solves this feel free to fix as you'd like. Sorry about the incorrect review 😅
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.
No worries, I think your idea makes more sense. Prefix is similar to sending requests to another host (which we were discussing to implement anyway). If we want to show the URL we used to send the request, probably transport should return back the URL used to send because there are additional parsing happening in transport anyway. Should be better to centralize all those URL parsing. I had hard time modifying this code 😄
@@ -203,6 +227,10 @@ const DebugPage: React.FunctionComponent<Props> = ({ | |||
)?.pathMapping || ''; | |||
} | |||
|
|||
if (urlPath.startsWith('/')) { | |||
urlPath = parseServerRootPath(docServiceRoute) + urlPath; |
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.
Question) Should Copy as Curl
also use the root path?
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 have the same behavior there as well, so yes.
44e491e
to
d99ee56
Compare
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 for the late review. 😉
Looks good overall. 👍
return CompletableFuture.supplyAsync(() -> { | ||
final DocStringSupport docStringSupport = new DocStringSupport(services); | ||
ServiceSpecification spec = generate(services); | ||
spec = docStringSupport.addDocStrings(spec); | ||
spec = exampleSupport.addExamples(spec); | ||
spec = exampleSupport.addExamples(spec, docServiceRoute); |
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.
Technically, docServiceRoute
isn't related to the example.
How about specifying it when calling generate
method above?
ServiceSpecification spec = generate(services, docServiceRoute);
// generate method
private ServiceSpecification generate(List<ServiceConfig> services, Route docServiceRoute) {
return ServiceSpecification.merge(
plugins.stream()
.map(plugin -> plugin.generateSpecification(
findSupportedServices(plugin, services),
filter, descriptiveTypeInfoProvider))
.collect(toImmutableList()), docServiceRoute);
}
// merge method
public static ServiceSpecification merge(Iterable<ServiceSpecification> specs, Route docServiceRoute) {
return new ServiceSpecification(
Streams.stream(specs).flatMap(s -> s.services().stream())::iterator,
Streams.stream(specs).flatMap(s -> s.enums().stream())::iterator,
Streams.stream(specs).flatMap(s -> s.structs().stream())::iterator,
Streams.stream(specs).flatMap(s -> s.exceptions().stream())::iterator,
ImmutableList.of(), docServiceRoute);
}
if (docServiceRoute != null && docServiceRoute.pathType() == RoutePathType.PREFIX) { | ||
return docServiceRoute; | ||
} |
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.
How about doing this in the constructor?
if (docServiceRoute != null && docServiceRoute.pathType() == RoutePathType.PREFIX) {
this.docServiceRoute = docServiceRoute;
} else {
this.docServiceRoute = null;
}
@@ -121,6 +133,8 @@ private static void generateDescriptiveTypeInfos( | |||
private final Set<StructInfo> structs; | |||
private final Set<ExceptionInfo> exceptions; | |||
private final List<HttpHeaders> exampleHeaders; | |||
@Nullable | |||
private Route docServiceRoute; |
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:
private Route docServiceRoute; | |
private final Route docServiceRoute; |
private final String proxyPath; | ||
|
||
private ProxyService(String proxyPath) { | ||
super(); |
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:
super(); |
@@ -328,7 +353,7 @@ const DebugPage: React.FunctionComponent<Props> = ({ | |||
`${window.location.port ? `:${window.location.port}` : ''}`; | |||
|
|||
const httpMethod = method.httpMethod; | |||
let uri; | |||
let baseUri; |
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.
let baseUri; | |
let mappedPath; |
because we use the term for this purpose? 😉
@@ -86,6 +86,7 @@ export default class AnnotatedHttpTransport extends Transport { | |||
bodyJson?: string, | |||
endpointPath?: string, | |||
queries?: string, | |||
pathPrefix?: string, |
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't this be pathPrefix
because we specify an empty string when it's 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.
Yep.
@minwoox finally had some time to address the issues while waiting in the airport 😄 Let me know what you think, excited to have this feature in the next release 😄 |
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.
Overall looks almost done 👍 It seems like the addExamples
is omitting the route causing test failures.
Can you check it?
@@ -144,9 +146,9 @@ export default abstract class Transport { | |||
* Default implementation is suitable for RPC, using endpoint.pathMapping === path. | |||
*/ | |||
protected validatePath(endpoint: Endpoint, path: string): { error?: string } { | |||
if (endpoint.pathMapping !== path) { | |||
if (!path.endsWith(endpoint.pathMapping)) { |
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.
Question) Since we decided to pass the pathPrefix
to the transport layer, I thought we can revert this change. Is there a code path which requires this change?
@@ -239,6 +239,7 @@ const MethodPage: React.FunctionComponent<Props> = (props) => { | |||
useRequestBody={needsToUseRequestBody(props.match.params.httpMethod)} | |||
debugFormIsOpen={debugFormIsOpen} | |||
setDebugFormIsOpen={setDebugFormIsOpen} | |||
docServiceRoute={props.specification.getDocServiceRoute()} |
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.
optional; I think also serverRoot
can be passed for dedup, but I'm fine with the current approach.
|
||
return window.location.pathname.substring( | ||
0, | ||
window.location.pathname.indexOf(docServicePath), |
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 it's possible that docServicePath
isn't even in the pathname
.
i.e. a proxy server just rewrites the path (i.e. foo
->bar
) instead of removing a prefix.
What do you think of just defaulting to no root path in this case?
const index = window.location.pathname.indexOf(docServicePath);
if (index < 0) {
return '';
}
return window.location.pathname.substring(0, index);
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.
it should happen rarely and it makes sense to default to that in that case.
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.
This looks great to me. Thanks!
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 good! Thanks @Dogacel 🙇 👍 🚀
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.
Excellent work and sorry for a late approval, @Dogacel. 🙇 We may want to disallow other route types than prefix, but it's probably something that we can take care of later.
Codecov ReportPatch coverage:
Additional details and impacted files@@ Coverage Diff @@
## main #4987 +/- ##
============================================
+ Coverage 74.22% 74.31% +0.09%
- Complexity 19518 19601 +83
============================================
Files 1674 1682 +8
Lines 71947 72260 +313
Branches 9217 9242 +25
============================================
+ Hits 53400 53702 +302
- Misses 14209 14217 +8
- Partials 4338 4341 +3
☔ 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.
Thanks, @Dogacel!
Motivation:
DocService
gRPC set host name explicitly #4482Modifications:
Add
cfg.route()
of theDocService
to theServiceSpecification
as an optional value.Result:
Example URL: http://127.0.0.1:9000/proxy/docs/#/methods/armeria.grpc.testing.TestService/UnaryCallWithAllDifferentParameterTypes/POST
Previously it sent requests to http://127.0.0.1:9000/test/armeria.grpc.testing.TestService/UnaryCallWithAllDifferentParameterTypes
but in reality it should be
URL: http://127.0.0.1:9000/proxy/test/armeria.grpc.testing.TestService/UnaryCallWithAllDifferentParameterTypes
Because
DocService
is bound under/docs
not/proxy/docs
.To emulate, you can create the following proxy service: