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

Change proxy handling to allow multiple proxies in development #1790

Merged
merged 12 commits into from
May 15, 2017
2 changes: 2 additions & 0 deletions packages/react-dev-utils/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -23,10 +23,12 @@
"launchEditor.js",
"openBrowser.js",
"openChrome.applescript",
"prepareProxy.js",
"WatchMissingNodeModulesPlugin.js",
"webpackHotDevClient.js"
],
"dependencies": {
"address": "1.0.1",
"anser": "1.1.0",
"babel-code-frame": "6.20.0",
"chalk": "1.1.3",
Expand Down
185 changes: 185 additions & 0 deletions packages/react-dev-utils/prepareProxy.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,185 @@
// @remove-on-eject-begin
/**
* Copyright (c) 2015-present, Facebook, Inc.
* All rights reserved.
*
* This source code is licensed under the BSD-style license found in the
* LICENSE file in the root directory of this source tree. An additional grant
* of patent rights can be found in the PATENTS file in the same directory.
*/
// @remove-on-eject-end
'use strict';

const address = require('address');
const chalk = require('chalk');
const url = require('url');

function resolveLoopback(proxy) {
const o = url.parse(proxy);
o.host = undefined;
if (o.hostname !== 'localhost') {
return proxy;
}
try {
o.hostname = address.ipv6() ? '::1' : '127.0.0.1';
} catch (_ignored) {
o.hostname = '127.0.0.1';
}
return url.format(o);
}

// We need to provide a custom onError function for httpProxyMiddleware.
// It allows us to log custom error messages on the console.
function onProxyError(proxy) {
return (err, req, res) => {
const host = req.headers && req.headers.host;
console.log(
chalk.red('Proxy error:') +
' Could not proxy request ' +
chalk.cyan(req.url) +
' from ' +
chalk.cyan(host) +
' to ' +
chalk.cyan(proxy) +
'.'
);
console.log(
'See https://nodejs.org/api/errors.html#errors_common_system_errors for more information (' +
chalk.cyan(err.code) +
').'
);
console.log();

// And immediately send the proper error response to the client.
// Otherwise, the request will eventually timeout with ERR_EMPTY_RESPONSE on the client side.
if (res.writeHead && !res.headersSent) {
res.writeHead(500);
}
res.end(
'Proxy error: Could not proxy request ' +
req.url +
' from ' +
host +
' to ' +
proxy +
' (' +
err.code +
').'
);
};
}

module.exports = function prepareProxy(proxy) {
// `proxy` lets you specify alternate servers for specific requests.
// It can either be a string or an object conforming to the Webpack dev server proxy configuration
// https://webpack.github.io/docs/webpack-dev-server.html
if (!proxy) return undefined;
if (typeof proxy !== 'object' && typeof proxy !== 'string') {
console.log(
chalk.red(
'When specified, "proxy" in package.json must be a string or an object.'
)
);
console.log(
chalk.red('Instead, the type of "proxy" was "' + typeof proxy + '".')
);
console.log(
chalk.red(
'Either remove "proxy" from package.json, or make it an object.'
)
);
process.exit(1);
}

// Otherwise, if proxy is specified, we will let it handle any request.
// There are a few exceptions which we won't send to the proxy:
// - /index.html (served as HTML5 history API fallback)
// - /*.hot-update.json (WebpackDevServer uses this too for hot reloading)
// - /sockjs-node/* (WebpackDevServer uses this for hot reloading)
// Tip: use https://jex.im/regulex/ to visualize the regex
const mayProxy = /^(?!\/(index\.html$|.*\.hot-update\.json$|sockjs-node\/)).*$/;

// Support proxy as a string for those who are using the simple proxy option
if (typeof proxy === 'string') {
if (!/^http(s)?:\/\//.test(proxy)) {
console.log(
chalk.red(
'When "proxy" is specified in package.json it must start with either http:// or https://'
)
);
process.exit(1);
}

let target;
if (process.platform === 'win32') {
target = resolveLoopback(proxy);
} else {
target = proxy;
}
return [
{
target,
logLevel: 'silent',
// For single page apps, we generally want to fallback to /index.html.
// However we also want to respect `proxy` for API calls.
// So if `proxy` is specified as a string, we need to decide which fallback to use.
// We use a heuristic: if request `accept`s text/html, we pick /index.html.
// Modern browsers include text/html into `accept` header when navigating.
// However API calls like `fetch()` won’t generally accept text/html.
// If this heuristic doesn’t work well for you, use a custom `proxy` object.
context: function(pathname, req) {
return mayProxy.test(pathname) &&
req.headers.accept &&
req.headers.accept.indexOf('text/html') === -1;
},
onProxyReq: proxyReq => {
// Browers may send Origin headers even with same-origin
// requests. To prevent CORS issues, we have to change
// the Origin to match the target URL.
if (proxyReq.getHeader('origin')) {
proxyReq.setHeader('origin', target);
}
},
onError: onProxyError(target),
secure: false,
changeOrigin: true,
ws: true,
xfwd: true,
},
];
}

// Otherwise, proxy is an object so create an array of proxies to pass to webpackDevServer
return Object.keys(proxy).map(function(context) {
if (!proxy[context].hasOwnProperty('target')) {
console.log(
chalk.red(
'When `proxy` in package.json is as an object, each `context` object must have a ' +
'`target` property specified as a url string'
)
);
process.exit(1);
}
let target;
if (process.platform === 'win32') {
target = resolveLoopback(proxy[context].target);
} else {
target = proxy[context].target;
}
return Object.assign({}, proxy[context], {
context: function(pathname) {
return mayProxy.test(pathname) && pathname.match(context);
},
onProxyReq: proxyReq => {
// Browers may send Origin headers even with same-origin
// requests. To prevent CORS issues, we have to change
// the Origin to match the target URL.
if (proxyReq.getHeader('origin')) {
proxyReq.setHeader('origin', target);
}
},
target,
onError: onProxyError(target),
});
});
};
110 changes: 65 additions & 45 deletions packages/react-scripts/config/webpackDevServer.config.js
Original file line number Diff line number Diff line change
Expand Up @@ -10,54 +10,74 @@
// @remove-on-eject-end
'use strict';

const launchEditor = require('react-dev-utils/launchEditor');
const config = require('./webpack.config.dev');
const paths = require('./paths');

const protocol = process.env.HTTPS === 'true' ? 'https' : 'http';
const host = process.env.HOST || 'localhost';
const host = process.env.HOST || '0.0.0.0';

module.exports = {
// Enable gzip compression of generated files.
compress: true,
// Silence WebpackDevServer's own logs since they're generally not useful.
// It will still show compile warnings and errors with this setting.
clientLogLevel: 'none',
// By default WebpackDevServer serves physical files from current directory
// in addition to all the virtual build products that it serves from memory.
// This is confusing because those files won’t automatically be available in
// production build folder unless we copy them. However, copying the whole
// project directory is dangerous because we may expose sensitive files.
// Instead, we establish a convention that only files in `public` directory
// get served. Our build script will copy `public` into the `build` folder.
// In `index.html`, you can get URL of `public` folder with %PUBLIC_URL%:
// <link rel="shortcut icon" href="%PUBLIC_URL%/favicon.ico">
// In JavaScript code, you can access it with `process.env.PUBLIC_URL`.
// Note that we only recommend to use `public` folder as an escape hatch
// for files like `favicon.ico`, `manifest.json`, and libraries that are
// for some reason broken when imported through Webpack. If you just want to
// use an image, put it in `src` and `import` it from JavaScript instead.
contentBase: paths.appPublic,
// By default files from `contentBase` will not trigger a page reload.
watchContentBase: true,
// Enable hot reloading server. It will provide /sockjs-node/ endpoint
// for the WebpackDevServer client so it can learn when the files were
// updated. The WebpackDevServer client is included as an entry point
// in the Webpack development configuration. Note that only changes
// to CSS are currently hot reloaded. JS changes will refresh the browser.
hot: true,
// It is important to tell WebpackDevServer to use the same "root" path
// as we specified in the config. In development, we always serve from /.
publicPath: config.output.publicPath,
// WebpackDevServer is noisy by default so we emit custom message instead
// by listening to the compiler events with `compiler.plugin` calls above.
quiet: true,
// Reportedly, this avoids CPU overload on some systems.
// https://github.com/facebookincubator/create-react-app/issues/293
watchOptions: {
ignored: /node_modules/,
},
// Enable HTTPS if the HTTPS environment variable is set to 'true'
https: protocol === 'https',
host: host,
overlay: false,
module.exports = function(proxy) {
return {
// Enable gzip compression of generated files.
compress: true,
// Silence WebpackDevServer's own logs since they're generally not useful.
// It will still show compile warnings and errors with this setting.
clientLogLevel: 'none',
// By default WebpackDevServer serves physical files from current directory
// in addition to all the virtual build products that it serves from memory.
// This is confusing because those files won’t automatically be available in
// production build folder unless we copy them. However, copying the whole
// project directory is dangerous because we may expose sensitive files.
// Instead, we establish a convention that only files in `public` directory
// get served. Our build script will copy `public` into the `build` folder.
// In `index.html`, you can get URL of `public` folder with %PUBLIC_URL%:
// <link rel="shortcut icon" href="%PUBLIC_URL%/favicon.ico">
// In JavaScript code, you can access it with `process.env.PUBLIC_URL`.
// Note that we only recommend to use `public` folder as an escape hatch
// for files like `favicon.ico`, `manifest.json`, and libraries that are
// for some reason broken when imported through Webpack. If you just want to
// use an image, put it in `src` and `import` it from JavaScript instead.
contentBase: paths.appPublic,
// By default files from `contentBase` will not trigger a page reload.
watchContentBase: true,
// Enable hot reloading server. It will provide /sockjs-node/ endpoint
// for the WebpackDevServer client so it can learn when the files were
// updated. The WebpackDevServer client is included as an entry point
// in the Webpack development configuration. Note that only changes
// to CSS are currently hot reloaded. JS changes will refresh the browser.
hot: true,
// It is important to tell WebpackDevServer to use the same "root" path
// as we specified in the config. In development, we always serve from /.
publicPath: config.output.publicPath,
// WebpackDevServer is noisy by default so we emit custom message instead
// by listening to the compiler events with `compiler.plugin` calls above.
quiet: true,
// Reportedly, this avoids CPU overload on some systems.
// https://github.com/facebookincubator/create-react-app/issues/293
watchOptions: {
ignored: /node_modules/,
},
// Enable HTTPS if the HTTPS environment variable is set to 'true'
https: protocol === 'https',
host: host,
overlay: false,
historyApiFallback: {
// Paths with dots should still use the history fallback.
// See https://github.com/facebookincubator/create-react-app/issues/387.
disableDotRule: true,
},
proxy,
setup(app) {
// This lets us open files from the crash overlay.
app.use(function launchEditorMiddleware(req, res, next) {
if (req.url.startsWith('/__open-stack-frame-in-editor')) {
launchEditor(req.query.fileName, req.query.lineNumber);
res.end();
} else {
next();
}
});
},
};
};
50 changes: 22 additions & 28 deletions packages/react-scripts/scripts/start.js
Original file line number Diff line number Diff line change
Expand Up @@ -35,7 +35,7 @@ const paths = require('../config/paths');
const config = require('../config/webpack.config.dev');
const devServerConfig = require('../config/webpackDevServer.config');
const createWebpackCompiler = require('./utils/createWebpackCompiler');
const addWebpackMiddleware = require('./utils/addWebpackMiddleware');
const prepareProxy = require('react-dev-utils/prepareProxy');

const useYarn = fs.existsSync(paths.yarnLockFile);
const cli = useYarn ? 'yarn' : 'npm';
Expand Down Expand Up @@ -73,34 +73,28 @@ function run(port) {
}
);

// Load proxy config
const proxy = require(paths.appPackageJson).proxy;
// Serve webpack assets generated by the compiler over a web sever.
const devServer = new WebpackDevServer(compiler, devServerConfig);

// Our custom middleware proxies requests to /index.html or a remote API.
addWebpackMiddleware(devServer)
.then(() => {
// Launch WebpackDevServer.
devServer.listen(port, HOST, err => {
if (err) {
return console.log(err);
}

if (isInteractive) {
clearConsole();
}
console.log(chalk.cyan('Starting the development server...'));
console.log();

openBrowser(`${protocol}://${HOST}:${port}/`);
});
})
.catch(e => {
console.log(
chalk.red('Failed to setup middleware, please report this error:')
);
console.log(e);
process.exit(1);
});
const devServer = new WebpackDevServer(
compiler,
devServerConfig(prepareProxy(proxy))
);

// Launch WebpackDevServer.
devServer.listen(port, HOST, err => {
if (err) {
return console.log(err);
}

if (isInteractive) {
clearConsole();
}
console.log(chalk.cyan('Starting the development server...'));
console.log();

openBrowser(`${protocol}://${HOST}:${port}/`);
});
}

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