Skip to content

Commit

Permalink
Allow custom proxies in development (#1790)
Browse files Browse the repository at this point in the history
* 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
  • Loading branch information
jamesblight authored and Timer committed May 15, 2017
1 parent 9b22817 commit 71a27c0
Show file tree
Hide file tree
Showing 5 changed files with 274 additions and 261 deletions.
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

0 comments on commit 71a27c0

Please sign in to comment.