Skip to content

Commit 71a27c0

Browse files
jamesblightTimer
authored andcommitted
Allow custom proxies in development (#1790)
* Change proxy handling to allow multiple proxies to be specified in package.json. * Up webpack-dev-server to 2.4.2 Webpack Dev Server version 2.4.2 handles the external websocket upgrade for all proxies * Fix the listen() call * Switch to correct default host * Remove promises and extract to react-dev-utils * oops
1 parent 9b22817 commit 71a27c0

File tree

5 files changed

+274
-261
lines changed

5 files changed

+274
-261
lines changed

packages/react-dev-utils/package.json

+2
Original file line numberDiff line numberDiff line change
@@ -23,10 +23,12 @@
2323
"launchEditor.js",
2424
"openBrowser.js",
2525
"openChrome.applescript",
26+
"prepareProxy.js",
2627
"WatchMissingNodeModulesPlugin.js",
2728
"webpackHotDevClient.js"
2829
],
2930
"dependencies": {
31+
"address": "1.0.1",
3032
"anser": "1.1.0",
3133
"babel-code-frame": "6.20.0",
3234
"chalk": "1.1.3",
+185
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,185 @@
1+
// @remove-on-eject-begin
2+
/**
3+
* Copyright (c) 2015-present, Facebook, Inc.
4+
* All rights reserved.
5+
*
6+
* This source code is licensed under the BSD-style license found in the
7+
* LICENSE file in the root directory of this source tree. An additional grant
8+
* of patent rights can be found in the PATENTS file in the same directory.
9+
*/
10+
// @remove-on-eject-end
11+
'use strict';
12+
13+
const address = require('address');
14+
const chalk = require('chalk');
15+
const url = require('url');
16+
17+
function resolveLoopback(proxy) {
18+
const o = url.parse(proxy);
19+
o.host = undefined;
20+
if (o.hostname !== 'localhost') {
21+
return proxy;
22+
}
23+
try {
24+
o.hostname = address.ipv6() ? '::1' : '127.0.0.1';
25+
} catch (_ignored) {
26+
o.hostname = '127.0.0.1';
27+
}
28+
return url.format(o);
29+
}
30+
31+
// We need to provide a custom onError function for httpProxyMiddleware.
32+
// It allows us to log custom error messages on the console.
33+
function onProxyError(proxy) {
34+
return (err, req, res) => {
35+
const host = req.headers && req.headers.host;
36+
console.log(
37+
chalk.red('Proxy error:') +
38+
' Could not proxy request ' +
39+
chalk.cyan(req.url) +
40+
' from ' +
41+
chalk.cyan(host) +
42+
' to ' +
43+
chalk.cyan(proxy) +
44+
'.'
45+
);
46+
console.log(
47+
'See https://nodejs.org/api/errors.html#errors_common_system_errors for more information (' +
48+
chalk.cyan(err.code) +
49+
').'
50+
);
51+
console.log();
52+
53+
// And immediately send the proper error response to the client.
54+
// Otherwise, the request will eventually timeout with ERR_EMPTY_RESPONSE on the client side.
55+
if (res.writeHead && !res.headersSent) {
56+
res.writeHead(500);
57+
}
58+
res.end(
59+
'Proxy error: Could not proxy request ' +
60+
req.url +
61+
' from ' +
62+
host +
63+
' to ' +
64+
proxy +
65+
' (' +
66+
err.code +
67+
').'
68+
);
69+
};
70+
}
71+
72+
module.exports = function prepareProxy(proxy) {
73+
// `proxy` lets you specify alternate servers for specific requests.
74+
// It can either be a string or an object conforming to the Webpack dev server proxy configuration
75+
// https://webpack.github.io/docs/webpack-dev-server.html
76+
if (!proxy) return undefined;
77+
if (typeof proxy !== 'object' && typeof proxy !== 'string') {
78+
console.log(
79+
chalk.red(
80+
'When specified, "proxy" in package.json must be a string or an object.'
81+
)
82+
);
83+
console.log(
84+
chalk.red('Instead, the type of "proxy" was "' + typeof proxy + '".')
85+
);
86+
console.log(
87+
chalk.red(
88+
'Either remove "proxy" from package.json, or make it an object.'
89+
)
90+
);
91+
process.exit(1);
92+
}
93+
94+
// Otherwise, if proxy is specified, we will let it handle any request.
95+
// There are a few exceptions which we won't send to the proxy:
96+
// - /index.html (served as HTML5 history API fallback)
97+
// - /*.hot-update.json (WebpackDevServer uses this too for hot reloading)
98+
// - /sockjs-node/* (WebpackDevServer uses this for hot reloading)
99+
// Tip: use https://jex.im/regulex/ to visualize the regex
100+
const mayProxy = /^(?!\/(index\.html$|.*\.hot-update\.json$|sockjs-node\/)).*$/;
101+
102+
// Support proxy as a string for those who are using the simple proxy option
103+
if (typeof proxy === 'string') {
104+
if (!/^http(s)?:\/\//.test(proxy)) {
105+
console.log(
106+
chalk.red(
107+
'When "proxy" is specified in package.json it must start with either http:// or https://'
108+
)
109+
);
110+
process.exit(1);
111+
}
112+
113+
let target;
114+
if (process.platform === 'win32') {
115+
target = resolveLoopback(proxy);
116+
} else {
117+
target = proxy;
118+
}
119+
return [
120+
{
121+
target,
122+
logLevel: 'silent',
123+
// For single page apps, we generally want to fallback to /index.html.
124+
// However we also want to respect `proxy` for API calls.
125+
// So if `proxy` is specified as a string, we need to decide which fallback to use.
126+
// We use a heuristic: if request `accept`s text/html, we pick /index.html.
127+
// Modern browsers include text/html into `accept` header when navigating.
128+
// However API calls like `fetch()` won’t generally accept text/html.
129+
// If this heuristic doesn’t work well for you, use a custom `proxy` object.
130+
context: function(pathname, req) {
131+
return mayProxy.test(pathname) &&
132+
req.headers.accept &&
133+
req.headers.accept.indexOf('text/html') === -1;
134+
},
135+
onProxyReq: proxyReq => {
136+
// Browers may send Origin headers even with same-origin
137+
// requests. To prevent CORS issues, we have to change
138+
// the Origin to match the target URL.
139+
if (proxyReq.getHeader('origin')) {
140+
proxyReq.setHeader('origin', target);
141+
}
142+
},
143+
onError: onProxyError(target),
144+
secure: false,
145+
changeOrigin: true,
146+
ws: true,
147+
xfwd: true,
148+
},
149+
];
150+
}
151+
152+
// Otherwise, proxy is an object so create an array of proxies to pass to webpackDevServer
153+
return Object.keys(proxy).map(function(context) {
154+
if (!proxy[context].hasOwnProperty('target')) {
155+
console.log(
156+
chalk.red(
157+
'When `proxy` in package.json is as an object, each `context` object must have a ' +
158+
'`target` property specified as a url string'
159+
)
160+
);
161+
process.exit(1);
162+
}
163+
let target;
164+
if (process.platform === 'win32') {
165+
target = resolveLoopback(proxy[context].target);
166+
} else {
167+
target = proxy[context].target;
168+
}
169+
return Object.assign({}, proxy[context], {
170+
context: function(pathname) {
171+
return mayProxy.test(pathname) && pathname.match(context);
172+
},
173+
onProxyReq: proxyReq => {
174+
// Browers may send Origin headers even with same-origin
175+
// requests. To prevent CORS issues, we have to change
176+
// the Origin to match the target URL.
177+
if (proxyReq.getHeader('origin')) {
178+
proxyReq.setHeader('origin', target);
179+
}
180+
},
181+
target,
182+
onError: onProxyError(target),
183+
});
184+
});
185+
};

packages/react-scripts/config/webpackDevServer.config.js

+65-45
Original file line numberDiff line numberDiff line change
@@ -10,54 +10,74 @@
1010
// @remove-on-eject-end
1111
'use strict';
1212

13+
const launchEditor = require('react-dev-utils/launchEditor');
1314
const config = require('./webpack.config.dev');
1415
const paths = require('./paths');
1516

1617
const protocol = process.env.HTTPS === 'true' ? 'https' : 'http';
17-
const host = process.env.HOST || 'localhost';
18+
const host = process.env.HOST || '0.0.0.0';
1819

19-
module.exports = {
20-
// Enable gzip compression of generated files.
21-
compress: true,
22-
// Silence WebpackDevServer's own logs since they're generally not useful.
23-
// It will still show compile warnings and errors with this setting.
24-
clientLogLevel: 'none',
25-
// By default WebpackDevServer serves physical files from current directory
26-
// in addition to all the virtual build products that it serves from memory.
27-
// This is confusing because those files won’t automatically be available in
28-
// production build folder unless we copy them. However, copying the whole
29-
// project directory is dangerous because we may expose sensitive files.
30-
// Instead, we establish a convention that only files in `public` directory
31-
// get served. Our build script will copy `public` into the `build` folder.
32-
// In `index.html`, you can get URL of `public` folder with %PUBLIC_URL%:
33-
// <link rel="shortcut icon" href="%PUBLIC_URL%/favicon.ico">
34-
// In JavaScript code, you can access it with `process.env.PUBLIC_URL`.
35-
// Note that we only recommend to use `public` folder as an escape hatch
36-
// for files like `favicon.ico`, `manifest.json`, and libraries that are
37-
// for some reason broken when imported through Webpack. If you just want to
38-
// use an image, put it in `src` and `import` it from JavaScript instead.
39-
contentBase: paths.appPublic,
40-
// By default files from `contentBase` will not trigger a page reload.
41-
watchContentBase: true,
42-
// Enable hot reloading server. It will provide /sockjs-node/ endpoint
43-
// for the WebpackDevServer client so it can learn when the files were
44-
// updated. The WebpackDevServer client is included as an entry point
45-
// in the Webpack development configuration. Note that only changes
46-
// to CSS are currently hot reloaded. JS changes will refresh the browser.
47-
hot: true,
48-
// It is important to tell WebpackDevServer to use the same "root" path
49-
// as we specified in the config. In development, we always serve from /.
50-
publicPath: config.output.publicPath,
51-
// WebpackDevServer is noisy by default so we emit custom message instead
52-
// by listening to the compiler events with `compiler.plugin` calls above.
53-
quiet: true,
54-
// Reportedly, this avoids CPU overload on some systems.
55-
// https://github.com/facebookincubator/create-react-app/issues/293
56-
watchOptions: {
57-
ignored: /node_modules/,
58-
},
59-
// Enable HTTPS if the HTTPS environment variable is set to 'true'
60-
https: protocol === 'https',
61-
host: host,
62-
overlay: false,
20+
module.exports = function(proxy) {
21+
return {
22+
// Enable gzip compression of generated files.
23+
compress: true,
24+
// Silence WebpackDevServer's own logs since they're generally not useful.
25+
// It will still show compile warnings and errors with this setting.
26+
clientLogLevel: 'none',
27+
// By default WebpackDevServer serves physical files from current directory
28+
// in addition to all the virtual build products that it serves from memory.
29+
// This is confusing because those files won’t automatically be available in
30+
// production build folder unless we copy them. However, copying the whole
31+
// project directory is dangerous because we may expose sensitive files.
32+
// Instead, we establish a convention that only files in `public` directory
33+
// get served. Our build script will copy `public` into the `build` folder.
34+
// In `index.html`, you can get URL of `public` folder with %PUBLIC_URL%:
35+
// <link rel="shortcut icon" href="%PUBLIC_URL%/favicon.ico">
36+
// In JavaScript code, you can access it with `process.env.PUBLIC_URL`.
37+
// Note that we only recommend to use `public` folder as an escape hatch
38+
// for files like `favicon.ico`, `manifest.json`, and libraries that are
39+
// for some reason broken when imported through Webpack. If you just want to
40+
// use an image, put it in `src` and `import` it from JavaScript instead.
41+
contentBase: paths.appPublic,
42+
// By default files from `contentBase` will not trigger a page reload.
43+
watchContentBase: true,
44+
// Enable hot reloading server. It will provide /sockjs-node/ endpoint
45+
// for the WebpackDevServer client so it can learn when the files were
46+
// updated. The WebpackDevServer client is included as an entry point
47+
// in the Webpack development configuration. Note that only changes
48+
// to CSS are currently hot reloaded. JS changes will refresh the browser.
49+
hot: true,
50+
// It is important to tell WebpackDevServer to use the same "root" path
51+
// as we specified in the config. In development, we always serve from /.
52+
publicPath: config.output.publicPath,
53+
// WebpackDevServer is noisy by default so we emit custom message instead
54+
// by listening to the compiler events with `compiler.plugin` calls above.
55+
quiet: true,
56+
// Reportedly, this avoids CPU overload on some systems.
57+
// https://github.com/facebookincubator/create-react-app/issues/293
58+
watchOptions: {
59+
ignored: /node_modules/,
60+
},
61+
// Enable HTTPS if the HTTPS environment variable is set to 'true'
62+
https: protocol === 'https',
63+
host: host,
64+
overlay: false,
65+
historyApiFallback: {
66+
// Paths with dots should still use the history fallback.
67+
// See https://github.com/facebookincubator/create-react-app/issues/387.
68+
disableDotRule: true,
69+
},
70+
proxy,
71+
setup(app) {
72+
// This lets us open files from the crash overlay.
73+
app.use(function launchEditorMiddleware(req, res, next) {
74+
if (req.url.startsWith('/__open-stack-frame-in-editor')) {
75+
launchEditor(req.query.fileName, req.query.lineNumber);
76+
res.end();
77+
} else {
78+
next();
79+
}
80+
});
81+
},
82+
};
6383
};

packages/react-scripts/scripts/start.js

+22-28
Original file line numberDiff line numberDiff line change
@@ -35,7 +35,7 @@ const paths = require('../config/paths');
3535
const config = require('../config/webpack.config.dev');
3636
const devServerConfig = require('../config/webpackDevServer.config');
3737
const createWebpackCompiler = require('./utils/createWebpackCompiler');
38-
const addWebpackMiddleware = require('./utils/addWebpackMiddleware');
38+
const prepareProxy = require('react-dev-utils/prepareProxy');
3939

4040
const useYarn = fs.existsSync(paths.yarnLockFile);
4141
const cli = useYarn ? 'yarn' : 'npm';
@@ -73,34 +73,28 @@ function run(port) {
7373
}
7474
);
7575

76+
// Load proxy config
77+
const proxy = require(paths.appPackageJson).proxy;
7678
// Serve webpack assets generated by the compiler over a web sever.
77-
const devServer = new WebpackDevServer(compiler, devServerConfig);
78-
79-
// Our custom middleware proxies requests to /index.html or a remote API.
80-
addWebpackMiddleware(devServer)
81-
.then(() => {
82-
// Launch WebpackDevServer.
83-
devServer.listen(port, HOST, err => {
84-
if (err) {
85-
return console.log(err);
86-
}
87-
88-
if (isInteractive) {
89-
clearConsole();
90-
}
91-
console.log(chalk.cyan('Starting the development server...'));
92-
console.log();
93-
94-
openBrowser(`${protocol}://${HOST}:${port}/`);
95-
});
96-
})
97-
.catch(e => {
98-
console.log(
99-
chalk.red('Failed to setup middleware, please report this error:')
100-
);
101-
console.log(e);
102-
process.exit(1);
103-
});
79+
const devServer = new WebpackDevServer(
80+
compiler,
81+
devServerConfig(prepareProxy(proxy))
82+
);
83+
84+
// Launch WebpackDevServer.
85+
devServer.listen(port, HOST, err => {
86+
if (err) {
87+
return console.log(err);
88+
}
89+
90+
if (isInteractive) {
91+
clearConsole();
92+
}
93+
console.log(chalk.cyan('Starting the development server...'));
94+
console.log();
95+
96+
openBrowser(`${protocol}://${HOST}:${port}/`);
97+
});
10498
}
10599

106100
// We attempt to use the default port but if it is busy, we offer the user to

0 commit comments

Comments
 (0)