I have a requirement of using Hapi with create-react-app where Hapi acts as a proxy for api requests and also serves the React app.
I'm trying to get the routing working, but it doesn't seem to work with the current Hapi configuration.
Here is my server code:
const Path = require('path');
const Hapi = require('hapi');
const Inert = require('inert');
const init = async () => {
const server = new Hapi.Server({
port: process.env.PORT || 5000,
routes: {
files: {
relativeTo: Path.join(__dirname, '../build')
}
}
});
await server.register(Inert);
server.route({
method: 'GET',
path: '/{param*}',
handler: {
directory: {
path: '.'
}
}
});
const options = {
ops: {
interval: 1000
},
reporters: {
myConsoleReporter: [
{
module: 'good-console',
args: [{ request: '*', response: '*' }]
},
'stdout'
]
}
};
await server.register({
plugin: require('good'),
options,
});
await server.start();
console.log('Server running at:', server.info.uri);
};
init();
The index.html file loads fine when localhost:5000 is open. I have configured a route /dashboard in the react-router part. Hitting localhost:5000/dashboard gives a 404.
Questions:
How do I configure the routes in Hapi so that React takes over the routing after the index.html is rendered?
The current server code serves the app from the build folder after the app is built. How do I configure it for hot reload without ejecting from the create-react-app
Note: The routing works when running the react app with npm start. But this is without the Hapi server running.
I'm new to using Hapi so any pointers are appreciated.
So I played around with various hapi+inert combo and this is what ended up working for me.
server.js
const Path = require('path');
const Hapi = require('hapi');
const Inert = require('inert');
const routes = require('./routes');
const init = async () => {
console.log('Routes are', routes);
const server = new Hapi.Server({
port: process.env.PORT || 5000,
routes: {
files: {
relativeTo: Path.join(__dirname, '../build')
}
}
});
await server.register(Inert);
server.route(routes);
/**
* This is required here because there are references to *.js and *.css in index.html,
* which will not be resolved if we don't match all remaining paths.
* To test it out, comment the code below and try hitting /login.
* Now that you know it doesn't work without the piece of code below,
* uncomment it.
*/
server.route({
method: 'GET',
path: '/{path*}',
handler: {
directory: {
path: '.',
redirectToSlash: true,
index: true,
}
}
});
const options = {
ops: {
interval: 1000
},
reporters: {
myConsoleReporter: [
{
module: 'good-console',
args: [{ request: '*', response: '*' }]
},
'stdout'
]
}
};
await server.register({
plugin: require('good'),
options,
});
await server.start();
console.log('Server running at:', server.info.uri);
};
init();
/routes/index.js
/**
* Use this for all paths since we just need to resolve index.html for any given path.
* react-router will take over and show the relevant component.
*
* TODO: add a 404 handler for paths not defined in react-router
*/
const fileHandler = {
handler: (req, res) => {
console.log(res.file('index.html'));
return res.file('index.html');
}
}
const routes = [
{ method: 'GET', path: '/login', config: fileHandler },
]
module.exports = routes;
The key thing to observe here is that for any named path (in this case /login) we always return the index.html file. For all other paths we tell hapi to return files from out of our build directory so that any references to *.css or *.js file in our index.hml will be resolved and we don't face a 404.
I'm not sure how react-router takes over the path resolution once index.html is loaded, but it beyond the scope of this question and is a topic of discussion for another time maybe.
As for the second question regarding hot-reload, I am still trying to figure it out. For now I am running both the hapi server and react-app independently, as I need /api for use in the react-app. Any suggestions or answers are welcome.
Here is how I did it. Tested it. Version "#hapi/hapi": "^20.0.1".
const path = require("path")
const Hapi = require('#hapi/hapi')
const Boom = require('#hapi/boom');
const server = Hapi.server({
port: 3000,
host: '0.0.0.0',
routes: {
files: {
relativeTo: path.join(__dirname, 'YOU BUILD REACT DIR')
}
}
});
(async () => {
await server.register([
require('vision'),
require('inert')
]);
server.route(
[{
method: 'GET',
path: '/{path*}',
options: {
ext: {
onPreResponse: {
method(req, h) {
//for other path prefix /Api
const isApi = req.path.substr(1)
.toLowerCase()
.trim()
.split('/')[0]
.replace(/\//g, "") === "api"
const response = req.response
if (response && req.response.output && req.response.output.statusCode === 404) {
if (isApi)
return Boom.notFound("Not Found")
return h.file('index.html');
}
return h.continue
},
}
}
},
handler: {
directory: {
path: ".",
listing: false,
index: true
}
}
},
{
method: 'GET',
path: '/Api/test',
handler: () => "OK"
}
])
await server.start();
console.log('Server running on %s', server.info.uri)
})()
Related
I setup sentry cloud in our React application but its blocked by AdBlockers (when turning Adblocker off, it works).
Is there someone who successfully setup a tunnel in a react application?
I played around with CORS but it didnĀ“t work
Playing around with the tunnel property in Sentry.init from the nextjs example in https://github.com/getsentry/examples/blob/master/tunneling/nextjs/pages/api/tunnel.js is throwing a /tunnel 404 (Not Found) console error in react app although I added a route to this path into my App which contains the handle function from nextjs example.
...
Sentry.init({
dsn: 'https://mine#mine.ingest.sentry.io/mine',
integrations: [new BrowserTracing()],
environment,
tunnel: '/tunnel',
tracesSampleRate,
});
...
where I tried it directly via <Route path='/tunnel' component={(req, res) => handle(req, res)} /> and also by using a component <Route path='/tunnel' component={Tunnel} /> with
function Tunnel(props) {
let location = useLocation();
useEffect(() => {
if(location.pathname === '/tunnel') {
handle(props.req, props.res);
}
}, [location.pathname]);
return null;
}
I even tried Webpack Plugin
plugins: [
new SentryWebpackPlugin({
include: '.',
ignore: ['node_modules'],
org: 'my_org',
project: 'app',
authToken:
'myToken',
}),
],
but it also is being getting blocked
--- Update ---
At least for local development and testing its possible to adjust the webpack config.
const bodyParser = require('body-parser')
const sentryHost = '#o<orgId>.ingest.sentry.io';
// Set knownProjectIds to an array with your Sentry project IDs which you
// want to accept through this proxy.
const knownProjectIds = ['12345'];
app.use(bodyParser.text());
app?.post('/tunnel', async (req, res) => {
try {
const envelope = req.body;
const pieces = envelope.split('\n');
const header = JSON.parse(pieces[0]);
// DSNs are of the form `https://<key>#o<orgId>.ingest.sentry.io/<projectId>`
const { host, pathname } = new URL(header.dsn);
// Remove leading slash
const projectId = pathname.substring(1);
if (host !== sentryHost) {
throw new Error(`invalid host: ${host}`);
}
if (!knownProjectIds.includes(projectId)) {
throw new Error(`invalid project id: $. {projectId}`);
}
const sentryIngestURL = `https://${sentryHost}/api/${projectId}/envelope/`;
const sentryResponse = await fetch(sentryIngestURL, {
method: 'POST',
body: envelope,
});
sentryResponse.headers.forEach(([key, value]) => res.setHeader(key, value));
res.status(sentryResponse.status).send(sentryResponse.body);
} catch (e) {
captureException(e);
return res.status(400).json({ status: 'invalid request' });
}
res.send("POST res sent from webpack dev server")
})
but only for local testing. In production I guess we would use a proxy.
i have one app on this url:
http://localhost:9000/test1
and one app on this url:
http://localhost:8081/test2
on the second app there is react app with js build file with name: "main.build.js"
everything work perfect when i enter to localhost:8081/test2.
i add in the first app http proxy middleware:
const proxy = require('http-proxy-middleware');
const devPort = 9000;
module.exports = function (env = {}) {
const ServerConfig = {
port: devPort,
historyApiFallback: true,
inline: true,
stats: {
colors: true,
exclude: ['node_modules']
},
setup: (server) => {
const middlewares = [];
middlewares.push(
proxy(['/test1'], {
target: 'http' + '://' + 'localhost' + ':' + '8081',
changeOrigin: true,
secure: false,
pathRewrite: {
'/test1': '/test2'
}
})
);
server.use(middlewares);
}
};
return ServerConfig;
};
and when I go to the URL
http://localhost:9000/test1
I expect to see the react app of localhost:8081/test2
but i see blank page with error in the console of 404 on the main.build.js file.
I tried a lot of combination with the URLs but nothing helped.
what can I do?
I have created a react (TS) using babel + webpack. It is working fine. Now I am converting it to a PWA. I followed this webpack article to updated webpack.config.js. To register the service worker, instead of doing it in index.html, I created a service-worker.tsx and referencing it in index.tsx.
I am getting this error
service-worker.ts:22 Uncaught ReferenceError: process is not defined
at register (service-worker.ts:22:33)
at eval (index.tsx:14:60)
at ./src/index.tsx (main.js:219:1)
at webpack_require (main.js:253:41)
at main.js:322:37
at main.js:324:12
I have added 3 files, where I changed the configuration to integrate workbox, for registering and un registering.
webpack.config.js
const HtmlWebPackPlugin = require("html-webpack-plugin");
const WorkboxPlugin = require("workbox-webpack-plugin");
const path = require("path");
// const htmlPlugin = new HtmlWebPackPlugin({
// template: "./src/index.html",
// filename: "./index.html",
// });
const htmlWebPackPlugin = new HtmlWebPackPlugin({
title: "PWA Webpack Demo",
template: "./src/index.html",
filename: "./index.html",
});
const workBoxPlugin_Generate = new WorkboxPlugin.GenerateSW({
// these options encourage the ServiceWorkers to get in there fast
// and not allow any straggling "old" SWs to hang around
clientsClaim: true,
skipWaiting: true,
});
module.exports = {
entry: "./src/index.tsx",
resolve: {
extensions: [".ts", ".tsx", ".js"],
},
output: {
// NEW
path: path.join(__dirname, "dist"),
filename: "[name].js",
}, // NEW Ends
plugins: [htmlWebPackPlugin, workBoxPlugin_Generate],
module: {
rules: [
{
test: /\.(js|jsx)$/,
exclude: /node_modules/,
use: {
loader: "babel-loader",
},
},
{
test: /\.(ts|tsx)$/,
exclude: /node_modules/,
use: ["ts-loader"],
},
// css rules
{
test: /\.css$/,
use: ["style-loader", "css-loader"],
},
],
},
};
service-worker.tsx (error at export default function register)
const isLocalhost = Boolean(
window.location.hostname === "localhost" ||
// [::1] is the IPv6 localhost address.
window.location.hostname === "[::1]" ||
// 127.0.0.1/8 is considered localhost for IPv4.
window.location.hostname.match(
/^127(?:\.(?:25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)){3}$/,
),
);
export default function register() {
if ("serviceWorker" in navigator) {
// The URL constructor is available in all browsers that support SW.
const publicUrl = new URL(
process.env.PUBLIC_URL!,
window.location.toString(),
);
if (publicUrl.origin !== window.location.origin) {
// Our service worker won't work if PUBLIC_URL is on a different origin
// from what our page is served on. This might happen if a CDN is used to
// serve assets; see https://github.com/facebookincubator/create-react-app/issues/2374
return;
}
window.addEventListener("load", () => {
const swUrl = `${process.env.PUBLIC_URL}/service-worker.js`;
if (isLocalhost) {
// This is running on localhost. Lets check if a service worker still exists or not.
checkValidServiceWorker(swUrl);
// Add some additional logging to localhost, pointing developers to the
// service worker/PWA documentation.
navigator.serviceWorker.ready.then(() => {
console.log(
"This web app is being served cache-first by a service " +
"worker.",
);
});
} else {
// Is not local host. Just register service worker
registerValidSW(swUrl);
}
});
}
}
function registerValidSW(swUrl: string) {
navigator.serviceWorker
.register(swUrl)
.then(registration => {
registration.onupdatefound = () => {
const installingWorker = registration.installing;
if (installingWorker) {
installingWorker.onstatechange = () => {
if (installingWorker.state === "installed") {
if (navigator.serviceWorker.controller) {
// At this point, the old content will have been purged and
// the fresh content will have been added to the cache.
// It's the perfect time to display a 'New content is
// available; please refresh.' message in your web app.
console.log("New content is available; please refresh.");
} else {
// At this point, everything has been precached.
// It's the perfect time to display a
// 'Content is cached for offline use.' message.
console.log("Content is cached for offline use.");
}
}
};
}
};
})
.catch(error => {
console.error("Error during service worker registration:", error);
});
}
function checkValidServiceWorker(swUrl: string) {
// Check if the service worker can be found. If it can't reload the page.
fetch(swUrl)
.then(response => {
// Ensure service worker exists, and that we really are getting a JS file.
if (
response.status === 404 ||
response.headers.get("content-type")!.indexOf("javascript") === -1
) {
// No service worker found. Probably a different app. Reload the page.
navigator.serviceWorker.ready.then(registration => {
registration.unregister().then(() => {
window.location.reload();
});
});
} else {
// Service worker found. Proceed as normal.
registerValidSW(swUrl);
}
})
.catch(() => {
console.log(
"No internet connection found. App is running in offline mode.",
);
});
}
export function unregister() {
if ("serviceWorker" in navigator) {
navigator.serviceWorker.ready.then(registration => {
registration.unregister();
});
}
}
index.tsx
import { createRoot } from 'react-dom/client';
import App from "./app";
//import * as serviceWorker from './serviceWorker';
import registerServiceWorker from './service-worker'
const container = document.getElementById('root')!
const root = createRoot(container)
root.render(<App />)
console.log('Registering service worker', 'Starting...')
registerServiceWorker()
After a little bit of research I found out that I needed to update the webpack.config.js with the following process definitions. It was not a problem of export as I was initially thinking. In my service worker I am using process.env entities.
In fact you can add other process.env parameters, I got my information from this article and this stackoverflow
In the webpack.config.js add the following:
const webpack = require("webpack");
const processEnvPlugin = new webpack.DefinePlugin({"process.env.PUBLIC_URL": JSON.stringify(process.env.PUBLIC_URL)});
plugins: [htmlWebPackPlugin, workBoxPlugin_Generate, processEnvPlugin]
I've got a basic NUXT app running, and during setup I opted to include the integrated Express Server.
I managed to get my SQL server database connected to Express (including Windows Authentication yah!)
I'm struggling to get the url to return data though. Using postman to hit the endpoint returns nuxt's built in 404 error page.
How do I configure NUXT to return the data from my query instead?
nuxt.config.js
const colors = require('vuetify/es5/util/colors').default
module.exports = {
mode: 'universal',
/*
** Headers of the page
*/
head: {
titleTemplate: '%s - ' + process.env.npm_package_name,
title: process.env.npm_package_name || '',
meta: [
{ charset: 'utf-8' },
{ name: 'viewport', content: 'width=device-width, initial-scale=1' },
{
hid: 'description',
name: 'description',
content: process.env.npm_package_description || ''
}
],
link: [{ rel: 'icon', type: 'image/x-icon', href: '/favicon.ico' }]
},
/*
** Customize the progress-bar color
*/
loading: { color: '#fff' },
/*
** Global CSS
*/
css: [],
/*
** Plugins to load before mounting the App
*/
plugins: [],
/*
** Nuxt.js dev-modules
*/
buildModules: [
// Doc: https://github.com/nuxt-community/eslint-module
'#nuxtjs/eslint-module',
'#nuxtjs/vuetify'
],
/*
** Nuxt.js modules
*/
modules: [
// Doc: https://axios.nuxtjs.org/usage
'#nuxtjs/axios'
],
/*
** Axios module configuration
** See https://axios.nuxtjs.org/options
*/
axios: {},
/*
** vuetify module configuration
** https://github.com/nuxt-community/vuetify-module
*/
vuetify: {
customVariables: ['~/assets/variables.scss'],
theme: {
dark: true,
themes: {
dark: {
primary: '#007B5F',
accent: '#007FA3',
secondary: '#33957F',
info: colors.teal.lighten1,
warning: colors.amber.base,
error: colors.deepOrange.accent4,
success: colors.green.accent3
}
}
}
},
/*
** Build configuration
*/
build: {
/*
** You can extend webpack config here
*/
extend(config, ctx) {}
}
}
These files are inside the root server folder
index.js
const express = require('express')
const consola = require('consola')
const { Nuxt, Builder } = require('nuxt')
const app = express()
// Import and Set Nuxt.js options
const config = require('../nuxt.config.js')
config.dev = process.env.NODE_ENV !== 'production'
// Add api routes
const bridgeRoute = require('./routes/bridgeRoute')
async function start() {
// Init Nuxt.js
const nuxt = new Nuxt(config)
const { host, port } = nuxt.options.server
// Build only in dev mode
if (config.dev) {
const builder = new Builder(nuxt)
await builder.build()
} else {
await nuxt.ready()
}
// Give nuxt middleware to express
app.use(nuxt.render)
// Use included routes
app.use('/api', bridgeRoute)
// Listen the server
app.listen(port, host)
consola.ready({
message: `Server listening on http://${host}:${port}`,
badge: true
})
}
start()
db.js
const sql = require('mssql/msnodesqlv8')
const consola = require('consola')
const dbConfig = require('./dbConfig')
// Swap between 'LOCAL', 'TEST' and 'PRODUCTION' environments
const connString = dbConfig.dbConfig.LOCAL.connectionString
const poolPromise = new sql.ConnectionPool(connString)
.connect()
.then((pool) => {
consola.success('Connected to MSSQL')
return pool
})
.catch((err) =>
consola.error('Database Connection Failed! Bad Config: ', err)
)
module.exports = {
sql,
poolPromise
}
dbConfig just houses the connection string to connect to SQL Server
/routes/bridgeRoute.js
const express = require('express')
const router = express.Router()
const { poolPromise } = require('../db')
router.get('api/bridges', async (req, res) => {
try {
const pool = await poolPromise
const result = await pool.request().query('SELECT TOP 10 FROM [BRIDGE]')
res.json(result)
} catch (err) {
res.status(500)
res.send(err.message)
}
})
module.exports = router
Into my component
axios.post('/api/' + 'create', {
name: 'new name'
},
{
headers:
{
'Content-Type': 'application/json'
}
}
)
into setupProxy.js , created from third part official instruction https://facebook.github.io/create-react-app/docs/proxying-api-requests-in-development
const proxy = require('http-proxy-middleware');
module.exports = function (app) {
app.use(proxy('/api/', {
target: 'http://my-api.com/',
changeOrigin: true
}));
};
When i call method with axios from my app
into browser console write
POST http://localhost:3000/api/create 404 (Not Found)
I tryed to write /api/* and /api/** into configuration http-proxy-middleware , but it did't help me.
What it does't work?
Please try using below code, with http-proxy-middleware version 1.0.0 onwards, you cannot use proxy as the name.
const { createProxyMiddleware } = require('http-proxy-middleware');
module.exports = (app) => {
app.use(createProxyMiddleware('/api',
{ target: 'http://localhost:3001/'
}));
}
Note: found this from one of the PR discussions here: https://github.com/facebook/create-react-app/issues/8550
I know its late and I came across the same issue. Keeping what worked for me so that others can give it a try.
I proxied this endpoint - https://services.odata.org/V2/Northwind/Northwind.svc/Customers?$format=json
setupProxy.js
const { createProxyMiddleware } = require('http-proxy-middleware');
module.exports = (app) => {
app.use(createProxyMiddleware('/api2', {
target: 'https://services.odata.org/V2/Northwind/Northwind.svc/',
changeOrigin: true,
pathRewrite: { '^/api2': '' }
})
);
}
Your .js file
triggerCORSCall() {
axios.get(`/api2/Customers?$format=json`)
.then(response => {
alert('Success');
}).catch(error => {
alert('Failure');
console.log(error);
})
}