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

New spdy option to enable/disable HTTP/2 with HTTPS #1721

Merged
merged 12 commits into from
Apr 4, 2019

Conversation

knagaitsev
Copy link
Collaborator

  • This is a bugfix
  • This is a feature
  • This is a code refactor
  • This is a test update
  • This is a docs update
  • This is a metadata update

For Bugs and Features; did you add new tests?

Yes

Motivation / Use-Case

I decided to try solving this problem for my first PR: #1713

I added the spdy option for the node API and the CLI, along with an http2 alias option on both which does the same thing. When spdy is enabled, https is enabled, and node version is below 10.0.0 (since spdy is broken beyond that), spdy will be used to create the server.

Breaking Changes

The dev server will no longer automatically serve over HTTP/2 when HTTPS is enabled and Node version is below 10.0.0, as the spdy/http2 option must be true for this to happen.

Additional Info

Changes in docs that I made to reflect the new options: https://github.com/Loonride/webpack.js.org/blob/dev-server-spdy-http2/src/content/configuration/dev-server.md#devserverspdy

@knagaitsev knagaitsev changed the title Http2 spdy New spdy option to enable/disable HTTP/2 with HTTPS Mar 16, 2019
@codecov
Copy link

codecov bot commented Mar 16, 2019

Codecov Report

Merging #1721 into master will decrease coverage by <.01%.
The diff coverage is 90%.

Impacted file tree graph

@@            Coverage Diff             @@
##           master    #1721      +/-   ##
==========================================
- Coverage   87.54%   87.54%   -0.01%     
==========================================
  Files           9        9              
  Lines         578      586       +8     
  Branches      170      173       +3     
==========================================
+ Hits          506      513       +7     
- Misses         60       61       +1     
  Partials       12       12
Impacted Files Coverage Δ
lib/utils/createConfig.js 95.6% <100%> (+0.09%) ⬆️
lib/Server.js 83.16% <87.5%> (ø) ⬆️

Continue to review full report at Codecov.

Legend - Click here to learn more
Δ = absolute <relative> (impact), ø = not affected, ? = missing data
Powered by Codecov. Last update 0984d4b...d05a3e5. Read the comment docs.

lib/Server.js Outdated
@@ -645,9 +644,14 @@ class Server {
// - https://github.com/nodejs/node/issues/21665
// - https://github.com/webpack/webpack-dev-server/issues/1449
// - https://github.com/expressjs/express/issues/3388
if (semver.gte(process.version, '10.0.0')) {
if (semver.gte(process.version, '10.0.0') || !options.spdy) {
Copy link
Member

Choose a reason for hiding this comment

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

I think !options.http2 should be added here.

});

it('Request to index', (done) => {
req.get('/').expect(200, /Heyo/, done);
Copy link
Member

Choose a reason for hiding this comment

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

Please check http/2 or not.

    it('Request to index', (done) => {
      req
        .get('/')
        .expect(200, /Heyo/)
        .then(({ res }) => {
          expect(res.httpVersion).toEqual('2.0');
          done();
        });
    });

Copy link
Collaborator Author

Choose a reason for hiding this comment

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

@hiroppy I've realized this is more difficult than it seems. Supertest sends an HTTP/1.1 request in the test, so the spdy server communicates back with 1.1. I confirmed in the browser that the protocol is h2 and spdy works. The two options I think are: use Node's built-in http2 module for the test and try to communicate with the server using it to confirm HTTP/2 works, or use puppeteer to test with a browser that supports HTTP/2 and confirm that the h2 protocol is used. What do you think?

});
});

afterEach(helper.close);
Copy link
Member

Choose a reason for hiding this comment

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

why is this necessary?

Copy link
Collaborator Author

Choose a reason for hiding this comment

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

Sorry, mistake from copying HTTPS tests over as a template. I will remove it.

Copy link
Member

@alexander-akait alexander-akait left a comment

Choose a reason for hiding this comment

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

See comment above, thanks!

if (options.spdy || options.http2) {
options.spdy = true;
options.http2 = true;
}
Copy link
Member

Choose a reason for hiding this comment

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

Let's separate spdy and http2 options, because it is misleading, spdy and https2 are difference protocols, better don't mix their

Copy link
Collaborator Author

Choose a reason for hiding this comment

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

@evilebottnawi I considered http2 an alias for spdy because http2 needs spdy to work at the moment. As an alternative, I could do this, to allow for future separation of http2 from spdy:

When ONLY http2 is enabled, the spdy server will be run (since this is the only way to run HTTP/2 on express at the moment), and these protocols will be provided to spdy:

options.https.spdy = {
  protocols: ['h2', 'http/1.1'],
};

Then the exact same thing will happen as above if http2 is enabled and spdy is enabled.

If ONLY spdy is enabled, the options.https.spdy will not be set unless the user specifies it themselves. If the user does not specify it in their options, it will go to the default spdy value as specified here:

protocols: ['h2','spdy/3.1', 'spdy/3', 'spdy/2','http/1.1', 'http/1.0']

Do you like this alternative better?

Copy link
Member

Choose a reason for hiding this comment

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

Ok, let's use the http2 option instead spdy. And we can introduce spdy under other option in future because spdy !== http2, also we need throw error if developer doesn't defined https when use http2: true

Copy link
Collaborator Author

Choose a reason for hiding this comment

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

Ok, that makes sense. Should user still be allowed to provide spdy options like this?:

{
  contentBase: './dist',
  watchContentBase: true,
  https: {
    spdy: {
      protocols: ['h2', 'http/1.1' ...]
    }
  },
  http2: true
}

Copy link
Member

Choose a reason for hiding this comment

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

@Loonride i thought about it, let's do this in other PR

Choose a reason for hiding this comment

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

@evilebottnawi i think it would help if the http2 option was nested under https because it makes it more obvious that http2 requires https. Perhaps even when only

{
    https: {
        http2: true,
    },
}

Treat that as if both https (with self-signed cert) & http2 are enabled?

Copy link
Member

Choose a reason for hiding this comment

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

hm, http2 !== https2, but no one browsers don't support http2 without https, but for avoid misleading better contain 2 option (i think), new developers can start think what http2 === https, but i am open for feedback

Choose a reason for hiding this comment

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

Actually, I like your suggestion below: assume https: true when http2: true and no https config is specified (and maybe output something in verbose logging about the assumption).

Choose a reason for hiding this comment

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

Oh, duh. That's exactly what this code is doing 🤦‍♂️

@knagaitsev
Copy link
Collaborator Author

knagaitsev commented Mar 19, 2019

I have removed the spdy option and instead just have the http2 option. I also made a test which depends on Node's experimental http2 module, since the only way to test an HTTP/2 server is to make an HTTP/2 request. I thought this was a reasonable choice since it will only force testers to have a Node version high enough for that, not normal users. If you think that is a problem, let me know. My alternative solution would be to dig into puppeteer and find the protocol of the request, similar to the way Chrome DevTools can show the protocol of a request.

Copy link
Member

@alexander-akait alexander-akait left a comment

Choose a reason for hiding this comment

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

Code looks really good, let's keep compatibility (no breaking change) and improve DX

lib/Server.js Outdated
options.spdy = true;
options.http2 = true;
if (options.http2 && !options.https) {
throw new Error("'https' option must be enabled to use HTTP/2");
Copy link
Member

Choose a reason for hiding this comment

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

I was wrong, it is poor DX, better set https: true in this case, no one browsers don't support http2 without ssl so, we can generate certificate sefely

lib/Server.js Outdated
}
options.https.spdy = {
protocols: ['h2', 'http/1.1'],
};
Copy link
Member

Choose a reason for hiding this comment

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

Unfortunately, it is breaking change, we can't remove spdy option in this version, spdy, how we should rewrite this part:

const isHttp2 = options.http2 || options.spdy
// Log deprecation `spdy` option.

if (semver.gte(process.version, '10.0.0') || !isHttp2) {
        this.listeningApp = https.createServer(options.https, app);
      } else {
        options.https.spdy = {
          protocols: ['http/1.1'],
        };

        // Allow to using https with `http1.1`
        if (isHttp2) { 
          options.https.spdy.protocols.unshift('h2');
        }

        /* eslint-disable global-require */
        // The relevant issues are:
        // https://github.com/spdy-http2/node-spdy/issues/350
        // https://github.com/webpack/webpack-dev-server/issues/1592
        this.listeningApp = require('spdy').createServer(options.https, app);
        /* eslint-enable global-require */
      }

Copy link
Collaborator Author

@knagaitsev knagaitsev Mar 20, 2019

Choose a reason for hiding this comment

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

@evilebottnawi I added the spdy option earlier in this PR, it wouldn't be a breaking change to remove it. Currently it looks like this in the code:

if (!options.https.spdy) {
        options.https.spdy = {
          protocols: ['h2', 'http/1.1'],
        };
      }

      if (semver.gte(process.version, '10.0.0')) {
        this.listeningApp = https.createServer(options.https, app);
      } else {
        /* eslint-disable global-require */
        // The relevant issues are:
        // https://github.com/spdy-http2/node-spdy/issues/350
        // https://github.com/webpack/webpack-dev-server/issues/1592
        this.listeningApp = require('spdy').createServer(options.https, app);
        /* eslint-enable global-require */
      }
    } else {
      this.listeningApp = http.createServer(app);
    }

Actually to avoid breaking changes, I think it should be like this:

const isHttp2 = options.http2 || options.https.spdy;
if (options.https.spdy) {
// log deprecation warning, this means they provided it as part of https option
}

if (semver.gte(process.version, '10.0.0') || !isHttp2) {
        this.listeningApp = https.createServer(options.https, app);
      } else {

        if (!options.https.spdy) {
          options.https.spdy = {
            protocols: ['h2', 'http/1.1'],
          };
        }

        /* eslint-disable global-require */
        // The relevant issues are:
        // https://github.com/spdy-http2/node-spdy/issues/350
        // https://github.com/webpack/webpack-dev-server/issues/1592
        this.listeningApp = require('spdy').createServer(options.https, app);
        /* eslint-enable global-require */
      }

So basically, user should still be allowed to use this: options.https.spdy. It was never in the docs, but it was possible to use it before. The only breaking change now would be that they used to get http2 automatically if they were below Node 10.0.0. Now, they have to set http2 to true to get http2.

Copy link
Member

Choose a reason for hiding this comment

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

👍

@@ -162,9 +162,6 @@
}
]
},
"spdy": {
"type": "boolean"
},
"http2": {
Copy link
Member

Choose a reason for hiding this comment

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

Don't remove spdy, we just new output deprecation as i written above

@@ -327,8 +324,7 @@
"disableHostCheck": "should be {Boolean} (https://webpack.js.org/configuration/dev-server/#devserver-disablehostcheck)",
"public": "should be {String} (https://webpack.js.org/configuration/dev-server/#devserver-public)",
"https": "should be {Object|Boolean} (https://webpack.js.org/configuration/dev-server/#devserver-https)",
"spdy": "should be {Boolean} (https://webpack.js.org/configuration/dev-server/#devserver-spdy)",
"http2": "should be {Boolean} (https://webpack.js.org/configuration/dev-server/#devserver-spdy)",
"http2": "should be {Boolean} (https://webpack.js.org/configuration/dev-server/#devserver-http2)",
"contentBase": "should be {Array} (https://webpack.js.org/configuration/dev-server/#devserver-contentbase)",
Copy link
Member

Choose a reason for hiding this comment

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

Don't remove, also we need create issue about documentation this (not related to PR right now, we should do this after merge)

if (argv.spdy) {
options.spdy = true;
if (argv.http2) {
options.http2 = true;
Copy link
Member

Choose a reason for hiding this comment

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

Same as above

Choose a reason for hiding this comment

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

Suggestion for a later/separate cleanup: There are a lot of if (argv.foo) options.foo = true. For the ones that are a simple copy:

const simpleOverrides = _.pick(argv, [
  'http2',
  // other simple if argv then set options
]);

_.merge(options, simpleOverrides);


expect(config).toMatchSnapshot();
});

it('http2 option', () => {
Copy link
Member

Choose a reason for hiding this comment

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

Don't remove this test, just move this in Http2 test

Copy link
Collaborator Author

Choose a reason for hiding this comment

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

I think this would stay in CreateConfig.test.js since it is only confirming that the config option appears after passing through the config helper, right? Http2 test should be for actual functionality of the server, not just confirming that the config options get through to the server

Copy link
Member

Choose a reason for hiding this comment

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

👍

'fixtures/contentbase-config/public'
);

describe('HTTP2', () => {
Copy link
Member

Choose a reason for hiding this comment

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

http2

@@ -494,6 +494,7 @@ exports[`createConfig http2 option 1`] = `
Object {
"hot": true,
"hotOnly": false,
"http2": true,
"https": true,
"noInfo": true,
"port": 8080,
Copy link
Member

Choose a reason for hiding this comment

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

Let's add spdy and http2

@knagaitsev
Copy link
Collaborator Author

@evilebottnawi It didn't pass CI for Node 6 because I used experimental http2 module for the test added in Node 8. Would it be fine to skip that test below the version where http2 was added, or should I find a new solution to test that HTTP/2 server works?

@knagaitsev
Copy link
Collaborator Author

Made a few changes. As I said before, options.spdy did not exist before this PR, so I don't think we should add it if it will just instantly be considered deprecated. Instead I think the options.https.spdy should be described as deprecated, even though it was never in the docs.

this.log.warn(
'Providing custom spdy server options is deprecated and will be removed in the next major version.'
);
} else {
Copy link
Member

Choose a reason for hiding this comment

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

spdy should enable h2 (don't do braking change, even when option doesn't exists in docs)

Deprecation message is ok 👍

Copy link
Collaborator Author

Choose a reason for hiding this comment

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

This is not a breaking change, if options.https.spdy is set by the user, it will not be changed at all currently: https://github.com/webpack/webpack-dev-server/blob/master/lib/Server.js#L650

lib/Server.js Outdated
// options.https.spdy is here for slightly better backwards compatability,
// since a user will probably provide this deprecated option if they
// are expecting the spdy server to be used.
const isHttp2 = options.http2 || options.https.spdy;
Copy link
Member

Choose a reason for hiding this comment

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

What do you think should we enable http2 be default when user use https: true (without http2 option) for better DX?

Copy link

Choose a reason for hiding this comment

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

I would prefer this since h2 is the standard for most CDNs going forward (or should be). Would be good to mimic the greater movement across the web. I originally opened the issue because I wrongly assumed I was getting h2 by default.

Copy link
Collaborator Author

Choose a reason for hiding this comment

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

I changed it so that h2 is only prevented if http2 is explicitly set to false.

@knagaitsev
Copy link
Collaborator Author

There should be no breaking changes at this point. I added another warning when someone tries to explicitly enable http2, but Node version is 10.0.0 is greater (where it does not work). I'm not sure what the second option is that @evilebottnawi was suggesting above. I think only http2 option should be added for now.

I think what you meant to say earlier is https !== http2. What I interpret as the misleading thing you are mentioning is that if a user wants to use http2, they have to put their key/cert stuff into options.https. Maybe the solution is that http2 should also take an object in the same way that options.https does. But I feel we should hold off on that until Express supports HTTP/2.

@JakobJingleheimer
Copy link

JakobJingleheimer commented Mar 30, 2019

Mhmm, putting the certs stuff inside http2 would make it obvious that SSL is required, and it align with node's http2:

const server = http2.createSecureServer({
  key: fs.readFileSync('localhost-privkey.pem'),
  cert: fs.readFileSync('localhost-cert.pem')
});
{
  http2: {
    key:  // …
    cert: // …
  }
}

So then https could just relate to http/1.1, and maybe both could have a true option that generates & uses a self-signed cert.

I think it's worth doing now.

But then what if someone includes both?

{
  https: {
    key:  // …
    cert: // …
  },
  http2: true,
}

Throw saying use one or the other? In the above, you could detect that https has certs and use those for http2 instead of generating new ones (and then warn that https and http2 options are mutually exclusive). That sounds like a can of worms though.

Copy link
Member

@alexander-akait alexander-akait left a comment

Choose a reason for hiding this comment

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

Great work, thanks!

@alexander-akait
Copy link
Member

/cc @hiroppy

Copy link
Member

@hiroppy hiroppy left a comment

Choose a reason for hiding this comment

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

Thanks!

@alexander-akait alexander-akait merged commit dcd2434 into webpack:master Apr 4, 2019
@knagaitsev knagaitsev added the gsoc Google Summer of Code label Aug 13, 2019
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
gsoc Google Summer of Code
Projects
None yet
Development

Successfully merging this pull request may close these issues.

6 participants