Skip to content
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

Introduce HttpResource service #50654

Closed
3 of 4 tasks
mshustov opened this issue Nov 14, 2019 · 10 comments
Closed
3 of 4 tasks

Introduce HttpResource service #50654

mshustov opened this issue Nov 14, 2019 · 10 comments
Labels
Feature:http Feature:New Platform Team:Core Core services & architecture: plugins, logging, config, saved objects, http, ES client, i18n, etc

Comments

@mshustov
Copy link
Contributor

mshustov commented Nov 14, 2019

Discussion in #44620

HttpResourceService

There are 2 different cases for static resources:

  • HTML page serving. We need to call a function to collect data and populate HTML page. Kibana configures the page with necessary headers (for example, CSP, CORS, etc.). HTML pages may be served from a "root-level" URL /my-page/.
  • static resources serving, like images/archives/etc. This use-case may use static file handlers in route declaration HTTP Service supports static files serving #41955 under the hood. We don't need a route handler, but a way to declare a file to serve / a directory to serve from.
httpResourceService.add({
  path: 'my-page', // served under '/my-page/'
  fromRoot: true,
  handler(){}
});
httpResourceService.add([{
  path: 'resource-name', // served under '/{pluginId}/resource-name/'
  directory: ...
}]);

Plan

@mshustov mshustov added blocker Team:Core Core services & architecture: plugins, logging, config, saved objects, http, ES client, i18n, etc Feature:New Platform labels Nov 14, 2019
@elasticmachine
Copy link
Contributor

Pinging @elastic/kibana-platform (Team:Platform)

@pgayvallet
Copy link
Contributor

Should this be a sub-service of http instead of a whole new service ?

http.resources.add({
  path: 'my-page', // server under '/my-page/'
  fromRoot: true,
  handler(){}
});

@mshustov
Copy link
Contributor Author

mshustov commented Mar 16, 2020

Should this be a sub-service of http instead of a whole new service ?

Let's look at HttpServiceSetup . If we want to have a common set of interceptors, auth/base path utils, etc. it sounds reasonable. However, we need to remove access to a plain router and provide HttpResource & HttpApi services instead.

HttpResource service use-cases:

To serve a static HTML page to bootstrap the Kibana app on the client.

  • via url
    Example 1
    Proposed API:
HttpResourceSetup.registerEntryPoint: ({ path: string }) => void
  • after performing some logic within a route handler
    Example:1 2
    Proposed API: extend ResponseFactory interface to render the core app:
http.resources.add({path: '/', validate:..., options: {auth:...}}, (context, req, res) => {
  return res.app.render()
})

to serve a dynamic HTML page.

Examples: 1 2
Proposed API: extend ResponseFactory interface to render a custom app:

http.resources.add({path: '/', validate:..., options: {auth:...}}, (context, req, res) => {
  return res.app.render({
    // will add CSP & 'cache-control': 'private, no-cache, no-store' headers
    body: `<!DOCTYPE html> ...`
  })
})

to serve static assets from a directory.

icons, images, fonts, etc.
Examples: 1 2 3
Proposed API:

HttpResourceSetup.registerStaticDir: ({ path: string, dirPath: string }) => void

It can be an internal API. We can enforce a convention for plugins to provide assets via the public/assets folder.

to serve dynamic assets

Examples:

http.resources.add({path: '/', validate:..., options: {auth:...}}, (context, req, res) => {
  // will add etag header
  return res.assets.custom({
    body: '..',
    headers: { 'content-type': 'text/javascript' }
  })
})

@joshdover
Copy link
Contributor

A sub-service / API makes sense to me. I think it makes finding these features much more ergonomic.

One thing that is a little inconsistent with these proposed APIs is that registering HTTP routes happens on a IRouter instance, rather than directly on the core.http object. Should we introduce these APIs on the IRouter interface for consistency?

Alternatively, what if we changed the http.createRouter().get(...) API to simply be http.router.get(...) (a router is created for the plugin automatically). This could be done later or as a follow up probably.


On the specific proposals above, I think they all make sense to me except the res.app.render() method:

http.resources.add({path: '/', validate:..., options: {auth:...}}, (context, req, res) => {
  return res.app.render()
})
http.resources.add({path: '/', validate:..., options: {auth:...}}, (context, req, res) => {
  return res.app.render({
    // will add CSP & 'cache-control': 'private, no-cache, no-store' headers
    body: `<!DOCTYPE html> ...`
  })
})

These two usages actually are different enough that I think they should be separate functions. The first renders the Core bootstrapping HTML page, while the second renders a custom HTML document. The only thing they both do is include the standard headers (CSP, cache-control). I also don't think the function for rendering an HTML document should be in the app namespace since it's not really an application in the way we use that term elsewhere.

Two APIs would make more sense to me: res.renderCoreApp() and res.renderHtml({ body: "<!DOCTYPE html> ..." }).

@mshustov
Copy link
Contributor Author

HttpResource & HttpApi service are supposed to be built on top of HttpService. However, adding additional helpers to KibanaResponseFactory only for HttpResources makes it incompatible with RequestHandler and HttpServiceInterceptors. Which in turn makes it impossible to share helpers for http-related API (for example, router.handleLegacyErrors https://github.com/elastic/kibana/blob/3bdbcd0d1a1a2a44bef3e006c6b596a282ea11c0/src/core/server/http/router/error_wrapper.ts, license checker #56926)
There are 3 possible solutions:

  • to duplicate helpers for different RequestHandler within HttpResource / HttpApi services. We aren't going to add HttpApi service soon, so we will have to have different helpers for HttpResources service and Router.
  • to introduce HttpResource service helpers (renderCoreApp, renderHtml, etc) in KibanaResponseFactory. That would require significant refactoring, as HttpService is a low-level service and doesn't depend on the other services (uiSettings, rendering, etc).
  • to use existing HttpRequestContext pattern to expose HttpResources API. The discoverability of the API is the downside of this approach.

I'd say that I'm fine to duplicate helpers for RequestHandler, but it means more work for plugins during migration. From a practical point of view, exposing HttpResources API via HttpRequestContext sounds like the most straightforward approach.

@joshdover
Copy link
Contributor

I'd say that I'm fine to duplicate helpers for RequestHandler, but it means more work for plugins during migration.

What extra work would there be for plugins? Is this the problem with route wrappers with different router types that may have different arguments?

Seems like it may be possible to create a wrapper that works for routers with different response factories:

type GenericRequestHandler<P, Q, B, Method, RouterArgs extends any[]> = (
  req: KibanaRequest<P, Q, B, Method>,
  ...args: RouterArgs
) => KibanaResponse | Promise<KibanaResponse>;

const wrapRoute = <P, Q, B, Method, RouterArgs extends any[]> = (
  handler: GenericRequestHandler<P, Q, B, Method, RouterArgs>
) => GenericRequestHandler<P, Q, B, Method, RouterArgs> {
  return (req, ...args: RouterArgs) => {
    // do pre-route things here
    // then run wrapped handler
    return handler(req, ...args);
  }
}

This pattern could also be extended for wrapping the entire IRouter interface(s). Though of course, changing the existing wrappers to work with this is still more work.

Is there something else about option (1) that creates more work for plugins?


Even still, I'm ok with leveraging HttpRequestContext for the time being and changing this later if it saves us significant amounts of time. Just not sure if changing this in the future is going to be a headache.

@pgayvallet
Copy link
Contributor

pgayvallet commented Apr 1, 2020

However, adding additional helpers to KibanaResponseFactory only for HttpResources makes it incompatible with RequestHandler and HttpServiceInterceptors. Which in turn makes it impossible to share helpers for http-related API

Just looked at https://github.com/elastic/kibana/pull/61797/files, and I was wondering if (yet another) layer of generics couldn't solve this issue

atm

export type RequestHandler<
  P = unknown,
  Q = unknown,
  B = unknown,
  Method extends RouteMethod = any
> = (
  context: RequestHandlerContext,
  request: KibanaRequest<P, Q, B, Method>,
  response: KibanaResponseFactory
) => IKibanaResponse<any> | Promise<IKibanaResponse<any>>;

addition in PR

export type HttpResourcesRequestHandler<P = unknown, Q = unknown, B = unknown> = (
  context: RequestHandlerContext,
  request: KibanaRequest<P, Q, B, 'get'>,
  response: KibanaResponseFactory & HttpResourcesServiceToolkit
) => IKibanaResponse<any> | Promise<IKibanaResponse<any>>;

what about

export type RequestHandler<
  P = unknown,
  Q = unknown,
  B = unknown,
  Method extends RouteMethod = any,
  ResponseFactory extends KibanaResponseFactory = KibanaResponseFactory,
> = (
  context: RequestHandlerContext,
  request: KibanaRequest<P, Q, B, Method>,
  response: ResponseFactory
) => IKibanaResponse<any> | Promise<IKibanaResponse<any>>;

and

export interface HttpResources {
  registerCoreApp: [...],
  register: <P, Q, B>(
    route: RouteConfig<P, Q, B, 'get'>,
    handler: RequestHandler<P, Q, B, 'get', HttpResourcesResponseFactory>
  ) => void;
}

or even

type HttpResourcesRequestHandler<P = unknown, Q = unknown, B = unknown> = RequestHandler<P, Q, B, 'get', HttpResourcesResponseFactory>;

export interface HttpResources {
  registerCoreApp: [...],
  register: <P, Q, B>(
    route: RouteConfig<P, Q, B, 'get'>,
    handler: HttpResourcesRequestHandler<P, Q, B) => void;
}

with HttpResourcesResponseFactory being a concrete implementation of KibanaResponseFactory & HttpResourcesServiceToolkit

  1. I think that would solve the incompatibility with the request handler wrappers or interceptor, can you confirm?

  2. If it does, I'm still unsure adding additional RequestHandler generic types every time we have to enhance/inherit/adapt/reuse it for yet another usage is going to be very maintainable long term. wdyt?

@mshustov
Copy link
Contributor Author

mshustov commented Apr 1, 2020

I think that would solve the incompatibility with the request handler wrappers or interceptor, can you confirm?

yeah, I don't want to overcomplicate the generics. However, it's not such a big problem if we provide wrapper type for a route handler from the core. Thus, plugins don't have to implement it manually
https://github.com/elastic/kibana/pull/61797/files#diff-6b2d2938215b66e48ab680c4f3515499R338

@mshustov
Copy link
Contributor Author

The last piece of the task is to serve a dynamic asset from a route handler. that's used in the canvas plugin

// const file = handler.file(SHAREABLE_RUNTIME_FILE, { confine: false });

The canvas plugin has a workaround in place. So we can switch to more urgent migration tasks meantime. In the future, we need to figure out how to solve the problem without using confine: false that looks like an error-prone approach.

@mshustov mshustov removed the blocker label Nov 5, 2020
@mshustov mshustov removed their assignment Aug 25, 2021
@lukeelmers
Copy link
Member

This has been sitting without activity for quite some time now, and all tasks have been addressed other than dynamic assets. As mentioned in #50654 (comment), canvas is working around this, so I'm going to close this for now. We can open a new issue in the future if more use cases for dynamic assets come up.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
Feature:http Feature:New Platform Team:Core Core services & architecture: plugins, logging, config, saved objects, http, ES client, i18n, etc
Projects
None yet
Development

No branches or pull requests

5 participants