-
Notifications
You must be signed in to change notification settings - Fork 1.1k
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
Openapi and api explorer to honor basePath #2554
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.
Thank you for the pull request.
The proposed change has few subtle ramifications that need further discussion, see my comments below.
|
||
const OPENAPI_SPEC_MAPPING: {[key: string]: OpenApiSpecForm} = { | ||
[`${this._basePath}/openapi.json`]: {version: '3.0.0', format: 'json'}, | ||
[`${this._basePath}/openapi.yaml`]: {version: '3.0.0', format: 'yaml'}, |
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 also not sure we want these endpoints to honor basePath
option.
@raymondfeng @strongloop/loopback-maintainers what's your opinion? When the application is configured with a custom basePath
, e.g. /api
, should we apply the base path to openapi endpoints too?
- Now we have
/openapi.json
and e.g./api/products
- Proposed change:
/api/openapi.json
and e.g./api/products
I am afraid the proposed implementation is a breaking change requiring a semver-major release. I think we should preserve backwards compatibility and introduce a new feature flag allowing users to decide whether to apply basePath configuration to openapi endpoints or not. By default, we should fall back to old behavior, but new projects scaffolded by lb4 cli
should enable the new behavior.
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.
@bajtos Having /explorer
always running at the root creates problems when there's a reverse proxy running in front of the API. Here's our situation and I'm sure this is pretty common case for many production environments.
We have both UI and API applications running on the same host behind nginx
.
UI runs at: http://www.mysite.com
Loopback API runs at: http://www.mysite.com/_api
Whenever a requests come for /_api/*
nginx
forwards it to Loopback API. All other requests to to the UI application.
As a result requests to http://www.mysite.com/explorer fail because they don't even reach the Loopback app. Hence my fix, which allows explorer to run at http://www.mysite.com/_api/explorer.
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 basePath
was introduced for this purpose. There are 3 paths that build a route to a controller method:
- server-level
basePath
: it's used as_expressApp.use(basePath, <lb4-rest-router>);
, such as/api
- controller-class-level
basePath
(set by @api): it's prepended to the path from step 3, such as/products
- controller-method-level
path
(set by @get, @post etc): local path of the operation, such as/{id}
Please note 1 is for express middleware mounting and it is transparent to our REST API routes. Express will strip /api
from request.url
property, which becomes /products/{id}
.
2 + 3 is for internal routing by @loopback/rest
.
Putting reverse proxy aside, the http url for a method is: /<serverBasePath>/<controllerBasePath>/<operationPath>
, such as /api/products/{id}
and api explorer is exposed at /explorer
by default.
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.
@raymondfeng Then my question is: is there a way to expose the API explorer at some different route (like /myprefix/explorer
)?
I did create a workaround in my project but it's quite hacky and involves messing with the express route stack directly. I'd like to avoid doing 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.
@dkrantsberg I'd like to better understand your scenario.
We have both UI and API applications running on the same host behind nginx.
- UI runs at: http://www.mysite.com
- Loopback API runs at: http://www.mysite.com/_api
Do you have two servers, one for UI and another for LoopBack API? If that's the case, then I would expect you should be able to configure your nginx instance as follows:
- API requests (e.g.
http://www.mysite.com/_api/products/{id}
) are forwarded to LB instance and_api
is stripped from the path (e.g./products/{id}
). - As a result, explorer will become available at
http://www.mysite.com/_api/explorer
. Alternatively, I believe it should be possible to configure nginx and define a new virtual route (e.g.http://www.mysite.com/api-explorer
) that will be forwarded to/explorer
in LB.
I think the key is to keep empty basePath
setting in LoopBack.
In case you are running both UI and LB API in the same server, how do you compose these two sets of routes?
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.
is there a way to expose the API explorer at some different route (like
/myprefix/explorer
)?
The self-hosted version of API Explorer that's provided by @loopback/rest-explorer
is already honoring basePath
setting and allows customization of the Explorer URL, see
https://github.com/strongloop/loopback-next/tree/master/packages/rest-explorer#basic-use
By default, API Explorer is mounted at /explorer. This path can be customized via RestExplorer configuration as follows:
this.bind(RestExplorerBindings.CONFIG).to({ path: '/openapi/ui', });
The redirection to externally-hosted API Explorer, which is built into @loopback/rest
, does not support customization of the URL and it also ignores basePath
setting. See here:
https://github.com/strongloop/loopback-next/blob/6e0eeb66b6a8c79d736fcc2dc7d9c76bfd57abd3/packages/rest/src/rest.server.ts#L288-L291
Please note that these redirects are deprecated, we want our users to use the self-hosted variant provided by @loopback/rest
.
@@ -920,11 +930,6 @@ export interface OpenApiSpecForm { | |||
format?: string; | |||
} | |||
|
|||
const OPENAPI_SPEC_MAPPING: {[key: string]: OpenApiSpecForm} = { | |||
'/openapi.json': {version: '3.0.0', format: 'json'}, |
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.
IMO, we should preserve OPENAPI_SPEC_MAPPING
as a constant config independent on basePath
. The basePath
setting should be applied when we are processing the mapping entries.
In your proposal, when app developers specify custom mapping, then their custom endpoints won't be prefixed with basePath
. I would find that surprising.
In my proposal, basePath
will be prepended to all mapping entries, regardless of whether they are provided by the user or come from the default mapping.
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.
Leaving OPENAPI_SPEC_MAPPING where it is and applying basePath
to it inside the RestServer constructor will mean recreating the entire OPENAPI_SPEC_MAPPING object with basePath
added to the key names.
Is there a reason to do it considering that OPENAPI_SPEC_MAPPING constant isn't used anywhere else in the code other than RestServer constructor?
Yes, I propose that custom mapping should be truly custom and not be altered once it is set by a developer. IMO this will give developers more flexibility. But I'm fine either way as long as we have this PR moving forward.
My main blocker is inability to customize /explorer
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.
My main blocker is inability to customize /explorer path.
Yes, that's what I'm seeing now. Please note @loopback/rest-explorer
mounts the UI as follows:
// explorePath is default to `/explorer`
application.static(explorerPath, swaggerUI.getAbsoluteFSPath());
But RestApplication.static()
adds it to a sub router of the root router (/api). As a result, the UI is only available under /api/explorer
while the spec is served at /openapi.json
. Here is the express pipeline:
/ (RestServer._expressApp)
/openapi.json
/openapi.yaml
/api
/products
/explorer (Explorer UI)
If we want keep /openapi.json
and /explorer
regardless of basePath
, we need to allow /explorer
to be mounted at root RestServer._expressApp
level.
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.
Leaving OPENAPI_SPEC_MAPPING where it is and applying basePath to it inside the RestServer constructor will mean recreating the entire OPENAPI_SPEC_MAPPING object with basePath added to the key names.
I am envisioning this differently. Instead of re-creating a new mapping object, we should apply basePath
when creating Express routes here:
- this._expressApp.get(p, (req, res) =>
+ this._expressApp.get(basePath + p, (req, res) =>
To make it clear, I was able to reproduce the problem by adding
|
Can you do both as a short term solution? |
I was thinking about this a bit, also in the context of reworking I am proposing to add a new flag to LB routes to allow them to ignore As for new RestApplication({
rest: {
basePath: '/api',
openApiSpec.endpointMapping: {
'/api/openapi.json': {version: '3.0.0', format: 'json'},
},
},
}); I acknowledge this is a not-very-pretty workaround, but it's something that works right now. For longer term, I am proposing to introduce a feature flag, re-posting part of my older comment:
|
@bajtos @raymondfeng @dkrantsberg |
@sanadHaj Thank you for chiming in. @dkrantsberg's use case is different. IIUC, his set up involves reverse proxies, which are not related to |
I would like to follow up with this issue.
@bajtos The workaround would not work as explorer will try to look for
In the index() {
let openApiSpecUrl = this.openApiSpecUrl;
if (this.request.baseUrl && this.request.baseUrl !== '/') {
openApiSpecUrl = this.request.baseUrl + openApiSpecUrl;
}
const data = {
openApiSpecUrl,
};
const homePage = templateFn(data);
this.response
.status(200)
.contentType('text/html')
.send(homePage);
} |
@gordancso thanks for chiming in!
This looks very reasonable to me. Could you please open a pull request to contribute this change? See Contributing code and Submitting a pull request to LoopBack 4 to get started. |
The expressApp.use('/lb4', lb4App); |
I found a little bit confusing here. Inside private async _serveOpenApiSpec(
request: Request,
response: Response,
specForm?: OpenApiSpecForm,
) {
const requestContext = new RequestContext(
request,
response,
this,
this.config,
);
specForm = specForm || {version: '3.0.0', format: 'json'};
let specObj = this.getApiSpec();
if (this.config.openApiSpec.setServersFromRequest) {
specObj = Object.assign({}, specObj);
specObj.servers = [{url: requestContext.requestedBaseUrl}];
}
const basePath = requestContext.basePath;
if (specObj.servers && basePath) {
for (const s of specObj.servers) {
// Update the default server url to honor `basePath`
if (s.url === '/') {
s.url = basePath;
}
}
}
if (specForm.format === 'json') {
const spec = JSON.stringify(specObj, null, 2);
response.setHeader('content-type', 'application/json; charset=utf-8');
response.end(spec, 'utf-8');
} else {
const yaml = safeDump(specObj, {});
response.setHeader('content-type', 'text/yaml; charset=utf-8');
response.end(yaml, 'utf-8');
}
} Currently the issue I have is that, after build, the API explorer looking at |
How did you do that? |
I tried to follow documentation Configure the Base Path for my LB4 application. There are actually two issues i found:
This issue only affects how explorer display the openapi spec. Endpoint-wise there's not issue but I will have to stick with older rest-explorer and rest packages whenever I need to build my application image. |
Sorry for the long delay. I have a ton of other work on my plate and have difficulties finding enough time and energy to take a close look at this complex issue. |
@gordancso I think it would help a lot if you could:
Without that, we will have to create such app ourselves, and that's something we don't have bandwidth for :( |
Current version of my dependencies:
Steps to Reproduce: Tried the below but no hope.
In all of the above, Expected Result Expect to be able to use explorer with |
I managed to reproduce the problem and opened a new pull request to fix it - see #2854 |
Closing as fixed via #2856 |
Checklist
👉 Read and sign the CLA (Contributor License Agreement) 👈
npm test
passes on your machinepackages/cli
were updatedexamples/*
were updated