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

[optimize] inject publicPath at request time #14007

Merged

Conversation

spalger
Copy link
Contributor

@spalger spalger commented Sep 15, 2017

Fixes #10724

Webpack bundles are now created using a placeholder for the publicPath rather than embedding the basePath directly into the assets. This means that:

  • the same build assets can be used for any basePath
  • we have to replace the placeholder at request time

The logic for serving these "dynamic" assets is completely based on hapijs/Inert, which is what was serving the assets before now. It's not exactly trivial but it does protect against path traversal attacks, produce and cache etags, efficiently use file descriptors, and doesn't require forking or hacking Inert/Hapi.

If we wanted to use a single handler for all files we would also need to add some of the lookup tricks supported by Inert's directory handler does, but I'm not convinced that's better.

@spalger spalger added Team:Core Core services & architecture: plugins, logging, config, saved objects, http, ES client, i18n, etc review v6.0.0 v6.1.0 v7.0.0 labels Sep 15, 2017
@epixa
Copy link
Contributor

epixa commented Sep 15, 2017

This is great, but it seems like way too large of a change to drop into 6.0. Let's target 6.1 so it can go through a full QA run.

@epixa
Copy link
Contributor

epixa commented Sep 15, 2017

I'm not against this approach as I do think we need a solution to this one way or another, but I can't help but feel like <base> is the right answer here, since it's the prescribed solution to this exact problem in the html spec itself. I'm not entirely sure why the approach was abandoned in #10854. @jbudz had mentioned that it doesn't impact relative paths in CSS files, but that shouldn't matter at all since paths in CSS files are relative to the location of the CSS file itself rather than the html document that loaded it, so this whole class of problem doesn't seem to apply there.

@spalger Do you think <base> is not an appropriate solution here? If so, why?

@spalger
Copy link
Contributor Author

spalger commented Sep 15, 2017

I was also hopeful for <base> but couldn't find any way to get it to work for CSS. It also broke a bunch of relative links, which felt like a very confusing bwc break.

@epixa
Copy link
Contributor

epixa commented Sep 15, 2017

To me, <base> seems like a large up front effort with a huge reward: a consistent spec'd behavior baked directly into browsers to solve a problem that may otherwise take 2 dependencies and 600 lines of code to support ourselves...

Again, I'm not against this particular change, but it seems like a stopgap to me. I'd like to better understand the issues around <base> though - do you remember what CSS problems you encountered? The tag shouldn't have any effect on relative paths inside css files, as those paths are relative to the css file itself rather than to the html document that linked to them.

@spalger
Copy link
Contributor Author

spalger commented Sep 16, 2017

Alright, I gave up too early on using <base>, but I'm still concerned about the changes necessary to get links working again. This is what the change set looks like for Kibana: master...spalger:base-tag

The worst breaking change is that links like <a href="#"> now link to <a href="/basepath#"> regardless of the current route, which means everything reloads the page and opens discover... Hopefully there aren't that many users using the #app/... style urls that we used extensively internally, but it also wouldn't surprise me. We might be able to use an href directive to fix some of them, or maybe a global click handler that tries to fix links automatically... or maybe we just want to pull off the bandaid...

@epixa
Copy link
Contributor

epixa commented Sep 16, 2017

If we do this, I think pulling off the bandaid is probably a good idea in the long run. We can create a blog post about the change in advance to provide details to plugin authors and whatnot, just like we did with the elasticsearch cluster changes in 5.2.

So the real question is whether we're OK with removing support for relative paths, which is effectively what <base> provides. I think the consistency that <base> provides is worth that limitation, but we should cast a wider net here to get feedback from the whole team.

@kimjoar
Copy link
Contributor

kimjoar commented Sep 18, 2017

++ I like the idea of moving to <base>.

Also agree on pulling off the bandaid. We shouldn't have links to <a href="#"> either way (the UI framework already contains styling for having a button look like a link).

@spalger spalger changed the base branch from master to 6.x September 20, 2017 00:46
@spalger spalger force-pushed the implement/request-time-public-path-replace branch from 6c65d67 to 1a16412 Compare September 20, 2017 00:47
@elastic elastic deleted a comment from karmi Sep 20, 2017
@spalger spalger changed the base branch from 6.x to master September 20, 2017 00:49
@spalger spalger force-pushed the implement/request-time-public-path-replace branch from 1a16412 to 9c31dbe Compare September 20, 2017 00:49
@spalger spalger added the v7.0.0 label Sep 25, 2017
@spalger
Copy link
Contributor Author

spalger commented Sep 25, 2017

Alright, we explored the idea of using <base> in #14045, and while it's certainly possible it also introduces a major breaking change in the way Kibana interprets URLs. Many of us were not comfortable with the impact of that change and many of us also want to fix #11015, which will have a similar impact, so we've tentatively agreed to try doing both changes at the same time in the 7.0 timeframe and plan to use this fix for now.

@kjbekkelund @epixa @jbudz @azasypkin who's excited to review this?!

@spalger spalger requested review from kimjoar and azasypkin and removed request for rhoboat September 25, 2017 17:39
@kimjoar
Copy link
Contributor

kimjoar commented Sep 26, 2017

👍 I'll try to get this reviewed tomorrow

Copy link
Contributor

@kimjoar kimjoar left a comment

Choose a reason for hiding this comment

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

LGTM. Added a couple comments that are semi-nitpicky. Tested on both Kibana and xpack and didn't hit any issues.


/**
* Get the hash of a file via a file descriptor
* @param {Fs.FileDescriptor} fd
Copy link
Contributor

Choose a reason for hiding this comment

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

Add the others params

* @return {undefined}
*/
export function createBundlesRoute({ bundlesPath, basePublicPath }) {
const cache = new LruCache(100);
Copy link
Contributor

Choose a reason for hiding this comment

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

Either too generically named or needs a comment, especially as it's not used in this file either. Maybe fileHashCache or something like that(?)

* @property {Hapi.Request} options.request
* @property {string} options.bundlesPath
* @property {string} options.publicPath
* @property {MapLike} [options.cache]
Copy link
Contributor

Choose a reason for hiding this comment

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

cache isn't optional, as far as I can tell

import { replacePlaceholder } from '../public_path_placeholder';

/**
* Create a Hapi response for the requested path
Copy link
Contributor

Choose a reason for hiding this comment

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

A bit more explanation maybe? Mention caching etc. Maybe pull in some of the info from your PR comment?

* @param {Object} options
* @property {string} options.bundlesPath
* @property {string} options.basePublicPath
* @return {undefined}
Copy link
Contributor

Choose a reason for hiding this comment

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

undefined is not correct, it's returning an object

@spalger
Copy link
Contributor Author

spalger commented Sep 28, 2017

Thanks for the feedback @kjbekkelund, should all be addressed. Hopefully @epixa @jbudz or @azasypkin have a chance to look into this soon 😄

Copy link
Member

@azasypkin azasypkin left a comment

Choose a reason for hiding this comment

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

LGTM, haven't noticed anything suspicious while testing so far.

// will store the 100 most recently used hashes.
const fileHashCache = new LruCache(100);

if (typeof bundlesPath !== 'string' || !isAbsolute(bundlesPath)) {
Copy link
Member

Choose a reason for hiding this comment

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

question: is typeof bundlesPath !== 'string check needed just to have more user friendly error message (in contrast to what node will throw by itself here and here)? I just find it useful that node uses require('util').inspect inside argument asserts :) No need to change anything, just curious.

Copy link
Contributor Author

@spalger spalger Oct 2, 2017

Choose a reason for hiding this comment

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

Yeah, I can include the bundlesPath value in the error message, but I figure the only time this will be triggered is when the user is monkeying around with the config file, so the more expressive error message will be more useful than seeing their value (and makes the error message longer, maybe wrap, etc.)


return {
method: 'GET',
path: `/bundles/{path*}`,
Copy link
Member

Choose a reason for hiding this comment

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

optional nit: quoting string with good old ' should be enough here.

onPreHandler: {
method(request, reply) {
const ext = extname(request.params.path);
if (ext !== '.js' && ext !== '.css') {
Copy link
Member

Choose a reason for hiding this comment

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

question: Should .js.map files be treated as .js files?

Copy link
Contributor Author

@spalger spalger Oct 2, 2017

Choose a reason for hiding this comment

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

Hmm... Good question... I'm tempted to say that since the placeholder only shows up in the webpack loader that processing sourcemap files isn't necessary. None of the source that is actually on disk should be using the placeholder, and if it happens to be the source maps shouldn't really be impacted outside of the line with the replacement.

import { open, fstat, createReadStream, close } from 'fs';

import Boom from 'boom';
import { fromNode as fcb } from 'bluebird';
Copy link
Member

Choose a reason for hiding this comment

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

question: since we started to use RxJS, maybe you can use Rx.Observable.bindNodeCallback + toPromise instead of Bluebird (IIRC we'd like to eventually get rid of this lib), what do you think?

Copy link
Contributor Author

@spalger spalger Oct 2, 2017

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 should start using RxJS outside of where it's especially helpful, yet. It's still unclear what direction we are going to go when it comes to which observable library we use

bundlesPath: '/bundles',
basePublicPath: 'a/'
});
}).to.throwError(/start and not end with a \//);
Copy link
Member

Choose a reason for hiding this comment

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

nit: could you please add case for the happy path too, as in the test above?

}

function assertBufferMatch(a, b) {
if (a.length !== b.length || a.indexOf(b) !== 0) {
Copy link
Member

Choose a reason for hiding this comment

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

question: why not just expect(a).to.eql(b) instead of assertBufferMatch? It seems to be supported...

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Huh, I guess I didn't check

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Makes sense I suppose, they're basically just arrays


expect(response.statusCode).to.be(200);
const image = readFileSync(resolve(outputFixture, 'image.png'));
expect(response.headers).to.have.property('content-length', image.length);
Copy link
Member

Choose a reason for hiding this comment

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

optional nit: our code is responsible for setting correct content-type now, maybe it makes sense to verify it here too.

@@ -0,0 +1,46 @@
import { Transform } from 'stream';
Copy link
Member

Choose a reason for hiding this comment

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

nit: would be great to have some unit tests for this file :)

buffer = buffer.slice(index + toReplace.length);
}

if (buffer.length > replacement.length) {
Copy link
Member

Choose a reason for hiding this comment

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

nit: could you please add comment explaining what this if case is for?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Nice, this was actually a bug: 8ca1c33

@spalger spalger merged commit 1ea82fa into elastic:master Oct 3, 2017
spalger added a commit that referenced this pull request Oct 3, 2017
* [optimize] inject publicPath at request time

* [optimize/getFileHash] finish doc block

* [optimize/bundlesRoute] correct return value doc type

* [optimize/bundleRoute] use more descriptive name for file hash cache

* [optimize/dynamicAssetResponse] add more details to doc

* [utils/createReplaceStream] trim the buffer based on the length of toReplace, not replacement

* [utils/createReplaceStream] add inline docs

* [utils/createReplaceStream] write unit tests

* [optimize/bundleRoute] expect supports buffers

* [optimize/bundleRoute/basePublicPath/tests] add happy path

* [optimize/bundlesRoute/tests] verify content-type header

* [optimize/bundlesRoute] use '

(cherry picked from commit 1ea82fa)
chrisronline pushed a commit to chrisronline/kibana that referenced this pull request Nov 20, 2017
* [optimize] inject publicPath at request time

* [optimize/getFileHash] finish doc block

* [optimize/bundlesRoute] correct return value doc type

* [optimize/bundleRoute] use more descriptive name for file hash cache

* [optimize/dynamicAssetResponse] add more details to doc

* [utils/createReplaceStream] trim the buffer based on the length of toReplace, not replacement

* [utils/createReplaceStream] add inline docs

* [utils/createReplaceStream] write unit tests

* [optimize/bundleRoute] expect supports buffers

* [optimize/bundleRoute/basePublicPath/tests] add happy path

* [optimize/bundlesRoute/tests] verify content-type header

* [optimize/bundlesRoute] use '
chrisronline pushed a commit to chrisronline/kibana that referenced this pull request Dec 1, 2017
* [optimize] inject publicPath at request time

* [optimize/getFileHash] finish doc block

* [optimize/bundlesRoute] correct return value doc type

* [optimize/bundleRoute] use more descriptive name for file hash cache

* [optimize/dynamicAssetResponse] add more details to doc

* [utils/createReplaceStream] trim the buffer based on the length of toReplace, not replacement

* [utils/createReplaceStream] add inline docs

* [utils/createReplaceStream] write unit tests

* [optimize/bundleRoute] expect supports buffers

* [optimize/bundleRoute/basePublicPath/tests] add happy path

* [optimize/bundlesRoute/tests] verify content-type header

* [optimize/bundlesRoute] use '
@spalger spalger deleted the implement/request-time-public-path-replace branch October 18, 2019 17:47
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
review Team:Core Core services & architecture: plugins, logging, config, saved objects, http, ES client, i18n, etc v6.1.0 v7.0.0
Projects
None yet
Development

Successfully merging this pull request may close these issues.

None yet

4 participants