MoonKnight
MoonKnight

Reputation: 23831

NGINX Setup to Match Proxy Middleware Configuration

I have the following setupProxy.js class that configures redirection for api calls to my server.

const { createProxyMiddleware } = require("http-proxy-middleware");

module.exports = function (app) {
    app.use(
        "/api/tours",
        createProxyMiddleware({
            target: "http://localhost:5000", 
            changeOrigin: true,
        })
    );
};

My App.js looks like

const App = () => {

    const { userDetails } = useContext(AuthContext);
    const { colorMode } = useContext(ColorModeContext);

    let currentTheme = React.useMemo(() =>
        createTheme(deepmerge(getDesignTokens(colorMode)), getThemedComponents(colorMode)),
        [colorMode]
    );
    currentTheme = responsiveFontSizes(currentTheme);
 
    return (
        <ThemeProvider theme={currentTheme}>
            <CssBaseline />
            <Router>
                <Switch>
                    <AuthRoute exact path="/" component={HomePage} />
                    <AuthRoute path="/home" component={HomePage} />
                    <Route path="/public/:id" component={PlayerPage} />
                    <AuthRoute path="/tours/:id" component={PlayerPage} />
                    <Route path="/login">
                        {userDetails == null ? <LoginPage /> : <Redirect to="/home" />}
                    </Route>
                    <Route component={FourOhFour} />
                </Switch>
            </Router>
        </ThemeProvider>
    );
};
export default App;

and I can navigate to my PlayerPage using a url like

localhost:3000/tours/eef67wsrr899iop009

This loads fine with the PlayerPage looking like

const PlayerPage = () => {

    const history = useHistory();

    const { id } = useParams();
    const mode = LocalStorgaeCache.getItem(APP_COLOR_MODE_KEY);
    const theme = createTheme(deepmerge(getDesignTokens(mode), getThemedComponents(mode)));
    
    const { userDetails } = useContext(AuthContext);
    const { tourDetails, isLoading: isLoadingTour, dispatch } = useContext(TourContext);

    const [loading, setLoading] = useState(true);
    const [showTagInfo, setTagId] = useState(0);
    const [tagInfoWindowVisible, showTagInfoWindow] = useState(false);
    const [results] = useState([]);
    const [setTour] = useState();
    const showTagging = useRef(false);
    const [showScan, setShowScan] = useState(false);
    const [addTag, setAddTag] = useState(false);
    const [tagging, setTagging] = useState(false);
    const [myPos, setPos] = useState([]);

    const myLayer = useRef(0);
    const tagEditing = useRef(false);
    const refreshTag = useRef(false);
    const [myTagNumber, setTagNumber] = useState(0);

    const [showPosInfo, setPosInfo] = useState('');
    const [snackMessage, setSnackMessage] = useState('');
    const [isMobile, setIsMobile] = useState(false);

    const [myTags, setTags] = useState([{}]);
    const [watermark, setWatermark] = useState();

    useEffect(() => {
        return () => {
            dispatch(clearLoadedTour());
        };
    }, []);

    useEffect(() => {
        setIsMobile(navigator.userAgent.match(/(iPad)|(iPhone)|(iPod)|(android)|(webOS)/i));

        const actualLoad = async () => {

            if (userDetails == null) { 
                history.push("/login");
            }
            await getTour(userDetails, id, dispatch);
            setLoading(false);
        };

        if (tourDetails == null) {
            actualLoad();
        }
        else {
            const simulateLoad = async () => {
                await new Promise((r) => setTimeout(r, 1000));
                setLoading(false);
            };
            simulateLoad();
        }
    }, [dispatch, history, id, tourDetails, userDetails]);

    return (
        <ThemeProvider theme={theme}>
            <Fragment>
                <Backdrop
                    sx={{ color: "white", zIndex: (theme) => theme.zIndex.drawer + 1 }}
                    open={loading}>
                    <CircularProgress color="inherit" />
                </Backdrop>
                { tourDetails != null ? (
                <div>
                    {!watermark && <Watermark />}
                    <SimpleSnackBar snackMessage={snackMessage} />
                    <InfoCard
                        tagEditing={tagEditing}
                        setTagging={setTagging}
                        setShow={showTagInfoWindow}
                        refreshTag={refreshTag}
                        setTour={setTour}
                        setShowScan={setShowScan}
                        results={results}
                        tagNumber={myTagNumber}
                        tags={myTags}
                        tour={tourDetails?.MyappTour}
                        pos={myPos}
                        setAddTag={setAddTag}
                        cb={showPosInfo}
                        st={showTagInfo}
                    />
                    {tagInfoWindowVisible && (
                        <MediaCard
                            tagEditing={tagEditing}
                            tagId={showTagInfo}
                            setShow={showTagInfoWindow}
                        />
                    )}
                    <Player
                        token={tourDetails?.azureServiceSasToken}
                        tour={tourDetails?.MyappTour}
                        account={tourDetails?.azureStorageUri}
                        setSnackMessage={setSnackMessage}
                        layer={myLayer}
                        tagging2={tagging}
                        refreshTag={refreshTag}
                        setTagId={setTagId}
                        showTagInfoWindow={showTagInfoWindow}
                        setPosInfo={setPosInfo}
                        setLoading={setLoading}
                        setNewTagPosition={setPos}
                        tagEditing={tagEditing}
                    />
                </div>
                ) : ( <></> ) }
            </Fragment>
        </ThemeProvider>
    );
};
export default PlayerPage;

The problem is, when I deploy this client and server with NGINX as the reverse-proxy and load balancer, the

myapp.com/tours/eef67wsrr899iop009

does not work, it merely gives a blank page and the browser console window is showing

Uncaught SyntaxError: Unexpected token '<' Manifest: Line: 1, column: 1, Syntax error.

My manifest.json file is

{
    "short_name": "Myapp",
    "name": "Myapp by vrpm",
    "icons": [
        {
            "src": "favicon.ico",
            "sizes": "64x64 32x32 24x24 16x16",
            "type": "image/x-icon"
        }
    ],
    "start_url": ".",
    "display": "standalone",
    "theme_color": "#000000",
    "background_color": "#ffffff"
}

and my index.html is

<!DOCTYPE html>
<html lang="en">
    <head>
        <title>Myapp</title>
        <meta charset="utf-8" />
        <meta name="theme-color" content="#000000" />
        <meta
            name="viewport"
            content="width=device-width, initial-scale=1, user-scalable=0, maximum-scale=1, minimum-scale=1"
        />
        <link rel="shortcut icon" href="%PUBLIC_URL%/favicon.ico" />
        <link rel="stylesheet" href="https://fonts.googleapis.com/css?family=Roboto:300,400,500,700&display=swap" />
        <link rel="manifest" href="%PUBLIC_URL%/manifest.json" />
    </head>
    <body>
        <noscript>You need to enable JavaScript to run this app.</noscript>
        <div id="root"></div>
    </body>
</html>

The NGINX configuration file looks as follows

worker_processes auto;

events {
  worker_connections 1024;
}

pid /var/run/nginx.pid;

http {

    include mime.types;

    upstream loadbalancer {
        server server:5000 weight=3;
    }

    server {
        listen 80 default_server;
        listen [::]:80 default_server;

        server_name _;

        port_in_redirect off;
        absolute_redirect off;

        return 301 https://$host$request_uri;
    }

    server {
        listen [::]:443 ssl;
        listen 443 ssl;

        server_name myapp.app* myapp.co* myapp-dev.uksouth.azurecontainer.io* localhost*;
        error_page 497 https://$host:$server_port$request_uri;

        error_log /var/log/nginx/client-proxy-error.log;
        access_log /var/log/nginx/client-proxy-access.log;

        ssl_protocols              TLSv1 TLSv1.1 TLSv1.2;
        ssl_ciphers                ECDHE-RSA-AES128-GCM-SHA256:ECDHE-ECDSA-AES128-GCM-SHA256:ECDHE-RSA-AES256-GCM-SHA384:ECDHE-ECDSA-AES256-GCM-SHA384:DHE-RSA-AES128-GCM-SHA256:DHE-DSS-AES128-GCM-SHA256:kEDH+AESGCM:ECDHE-RSA-AES128-SHA256:ECDHE-ECDSA-AES128-SHA256:ECDHE-RSA-AES128-SHA:ECDHE-ECDSA-AES128-SHA:ECDHE-RSA-AES256-SHA384:ECDHE-ECDSA-AES256-SHA384:ECDHE-RSA-AES256-SHA:ECDHE-ECDSA-AES256-SHA:DHE-RSA-AES128-SHA256:DHE-RSA-AES128-SHA:DHE-DSS-AES128-SHA256:DHE-RSA-AES256-SHA256:DHE-DSS-AES256-SHA:DHE-RSA-AES256-SHA:AES128-GCM-SHA256:AES256-GCM-SHA384:ECDHE-RSA-RC4-SHA:ECDHE-ECDSA-RC4-SHA:AES128:AES256:RC4-SHA:HIGH:!aNULL:!eNULL:!EXPORT:!DES:!3DES:!MD5:!PSK;
        ssl_prefer_server_ciphers  on;
        ssl_session_cache          shared:SSL:10m;
        ssl_session_timeout        24h;

        keepalive_timeout 300;
        add_header Strict-Transport-Security 'max-age=31536000; includeSubDomains';

        ssl_certificate     /etc/nginx/certificate.crt;
        ssl_certificate_key /etc/nginx/private.key;

        root /usr/share/nginx/html;
        index index.html index.htm index.nginx-debian.html;

         location / {
            proxy_http_version 1.1;
            proxy_set_header Upgrade $http_upgrade;
            proxy_set_header Connection 'upgrade';
            proxy_set_header Host $host;
            proxy_cache_bypass $http_upgrade;
            try_files $uri $uri/ /index.html;
        }

         location /api/auth {
            proxy_set_header Upgrade $http_upgrade;
            proxy_set_header Connection 'upgrade';
            proxy_set_header Host $host;
            proxy_set_header X-Real-IP $remote_addr;
            proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
            proxy_set_header X-Forwarded-Proto $scheme;
            proxy_pass http://loadbalancer;
        }
        
        location /api/tours {
            proxy_set_header Upgrade $http_upgrade;
            proxy_set_header Connection 'upgrade';
            proxy_set_header Host $host;
            proxy_set_header X-Real-IP $remote_addr;
            proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
            proxy_set_header X-Forwarded-Proto $scheme;
            proxy_pass http://loadbalancer;
        }
    }
}

Locally, this configuration works fine and there is no suggestion that anything is wrong with my manifest.json or my index.html files. So the question I think must be:

How can I ammend my NGINX proxying to allow for direct routing via the client using the url myapp.com/tours/<some-id>?


Edit. Note, in my AuthRoute, I log the request path using the following code

import { useContext } from "react";
import { AuthContext } from "../context/auth/authContext";
import { Route, Redirect } from "react-router-dom";

const AuthRoute = ({ component: Component, ...rest }) => {
    const authContext = useContext(AuthContext);
    const { isAuthenticated, isAuthorizing } = authContext;
    
    console.log(`Navigation request for ${window.location.href}`);

    return (
        <Route
            {...rest}
            render={props =>
                !isAuthenticated && !isAuthorizing ? (<Redirect to="/login" />) : (<Component {...props} />)
            }
        />
    );
};
export default AuthRoute;

locally this prints out the requested route and it is navgated to; so

Navigation request for https://localhost:3000/tours/3r4et66ksop093jsn

On Azure when deployed, these messages are never displayed which suggests that NGINX is intervieening.


I have implemented the suggested changes and my .config now looks like

worker_processes auto;

events {
  worker_connections 1024;
}

pid /var/run/nginx.pid;

http {

    include mime.types;

    upstream loadbalancer {
        server server:5000 weight=3;
    }

    server {
        listen 80 default_server;
        listen [::]:80 default_server;

        server_name _;

        port_in_redirect off;
        absolute_redirect off;

        return 301 https://$host$request_uri;
    }

    server {
        listen [::]:443 ssl;
        listen 443 ssl;

        server_name viewform.app* viewform.co* viewform-dev.uksouth.azurecontainer.io* localhost*;
        error_page 497 https://$host:$server_port$request_uri;

        error_log /var/log/nginx/client-proxy-error.log;
        access_log /var/log/nginx/client-proxy-access.log;

        ssl_protocols              TLSv1 TLSv1.1 TLSv1.2;
        ssl_ciphers                ECDHE-RSA-AES128-GCM-SHA256:ECDHE-ECDSA-AES128-GCM-SHA256:ECDHE-RSA-AES256-GCM-SHA384:ECDHE-ECDSA-AES256-GCM-SHA384:DHE-RSA-AES128-GCM-SHA256:DHE-DSS-AES128-GCM-SHA256:kEDH+AESGCM:ECDHE-RSA-AES128-SHA256:ECDHE-ECDSA-AES128-SHA256:ECDHE-RSA-AES128-SHA:ECDHE-ECDSA-AES128-SHA:ECDHE-RSA-AES256-SHA384:ECDHE-ECDSA-AES256-SHA384:ECDHE-RSA-AES256-SHA:ECDHE-ECDSA-AES256-SHA:DHE-RSA-AES128-SHA256:DHE-RSA-AES128-SHA:DHE-DSS-AES128-SHA256:DHE-RSA-AES256-SHA256:DHE-DSS-AES256-SHA:DHE-RSA-AES256-SHA:AES128-GCM-SHA256:AES256-GCM-SHA384:ECDHE-RSA-RC4-SHA:ECDHE-ECDSA-RC4-SHA:AES128:AES256:RC4-SHA:HIGH:!aNULL:!eNULL:!EXPORT:!DES:!3DES:!MD5:!PSK;
        ssl_prefer_server_ciphers  on;
        ssl_session_cache          shared:SSL:10m;
        ssl_session_timeout        24h;

        keepalive_timeout 300;
        add_header Strict-Transport-Security 'max-age=31536000; includeSubDomains';

        ssl_certificate     /etc/nginx/certificate.crt;
        ssl_certificate_key /etc/nginx/private.key;

        root /usr/share/nginx/html;
        index index.html index.htm index.nginx-debian.html;

        location /api/auth {
            proxy_set_header Upgrade $http_upgrade;
            proxy_set_header Connection 'upgrade';
            proxy_set_header Host $host;
            proxy_set_header X-Real-IP $remote_addr;
            proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
            proxy_set_header X-Forwarded-Proto $scheme;
            proxy_pass http://loadbalancer;
        }
        
        location /api/tours {
            proxy_set_header Upgrade $http_upgrade;
            proxy_set_header Connection 'upgrade';
            proxy_set_header Host $host;
            proxy_set_header X-Real-IP $remote_addr;
            proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
            proxy_set_header X-Forwarded-Proto $scheme;
            proxy_pass http://loadbalancer;
        }

        location / {
            try_files $uri /index.html;
        }
    }
}

Now when I navigate to dev.myapp/public/tours/ I get a blank page as shown below

enter image description here

with the following warnings

Uncaught SyntaxError: Unexpected token '<' manifest.json:1 Manifest: Line: 1, column: 1, Syntax error.

My index.html and manifest.json is above.

Upvotes: 1

Views: 2463

Answers (1)

Chandan
Chandan

Reputation: 11797

Since nginx don't know about tours path which is handled by react-router you can either tell nginx to reroute all the request to / root route which then passes the request to index.html or you can add regex path and route all the request to / root location in nginx.conf. Since now / root location is handling all the other request may not get passed to other location so it would be best to move current route below static location in nginx.conf.

Below is how to route all request to index.html file nginx.conf

...
location /api/auth {
  proxy_set_header Upgrade $http_upgrade;
  proxy_set_header Connection 'upgrade';
  proxy_set_header Host $host;
  proxy_set_header X-Real-IP $remote_addr;
  proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
  proxy_set_header X-Forwarded-Proto $scheme;
  proxy_pass http://loadbalancer;
}

location /api/tours {
  proxy_set_header Upgrade $http_upgrade;
  proxy_set_header Connection 'upgrade';
  proxy_set_header Host $host;
  proxy_set_header X-Real-IP $remote_addr;
  proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
  proxy_set_header X-Forwarded-Proto $scheme;
  proxy_pass http://loadbalancer;
}

location / {
     try_files $uri /index.html; # change as below
}
...

OP Edit: In my package.json "homepage": ".", needed to be "homepage": "/",. This is what was causing the routing to fail - sneaky. Hopefully this helps someone else.

Upvotes: 1

Related Questions