41

I have a little project in reactjs to play with and learn it. I need to have to type of headers which will be shown based on url paths. So the following is my index.js which handles the routing:

 const history = useRouterHistory(createHistory)({
     basename: '/test'
})
class Tj extends React.Component {

render() {

    return (
        <Router history={history}>
            <Route path={"/"} component={Bridge} >
                <IndexRoute component={Home} />
                <Route path={"user"} component={T} />
                <Route path={"home"} component={Home} />
            </Route>
            <Route path={"/t/"} component={Bridge2} >
                <IndexRoute component={T} />
                <Route path={"contact"} component={Home} />
            </Route>
        </Router>
    );
}
}
render(
<Provider store={store}>
    <Tj/>
</Provider>,
window.document.getElementById('mainContainer'));

As you can see I am using test as a root directory and based on user's input for url I decide which header should I use. Also here is Bridge2.js:

export class Bridge2 extends React.Component {
render() {

    return (
        <div>
            <div className="row">
                <Header2/>
            </div>
            <div className="row">
                {this.props.children}
            </div>
        </div>
    );
}
}

and Bridge.js:

export class Bridge extends React.Component {
render() {
  //  alert(this.props.children.type);
    var Content;

    if(this.props.children.type){
    Content=<div>
        <div className="row">
            <Header/>
        </div>
        <div className="row">
            {this.props.children}
        </div>
    </div>;
    }
    else{
        Content=<div>
            <div className="row">
                <Header/>
            </div>
            <div className="row">
                {this.props.children}
            </div>
        </div>;
    }
    return (
        Content
    );
}
}

When I run this in webpack dev server every thing works fine. For instance when I use http://localhost:3003/test/ bridge.js is loaded and if I run http://localhost:3003/test/t/ bridge2.js is loaded which is expected.

However since web pack dev server is not a production server I use tomcat and for now I use the eclipse web application project and I copied my bundle.js file and index.html over there. Now the problem is when I run the tomcat server it is able to recognize and show this path fine:

http://localhost:8080/test/ but when for http://localhost:8080/test/t/ I get:

HTTP Status 404 - /test/t/

which basically says the resource file is not available. As far as what I observe this is not a problem in coding since routing works fine in web pack dev server but when it comes to tomcat it seems that react routing is not able to handle it. Is there anything wrong with what I am doing? Is doable this way at all?

Nimantha
  • 6,405
  • 6
  • 28
  • 69
Hamed Minaee
  • 2,480
  • 4
  • 35
  • 63

9 Answers9

72

First, you must be aware of the following differences when using react-router.

When you enter localhost:3003/test/ in your browser, it will request the server, and then it will receive /test/index.html, the js bundle, css, ...

After that, whenever you click an internal link (eg. localhost:3003/test/t/), your browser will not request the server again. React-router will resolve this client-side, re-render portions of the page, and update browser's address bar (using HTML5 pushstate()), without triggering another server request.

When you enter localhost:3003/test/t/ directly in the address bar, your browser will request the server, and Tomcat does not have /test/t/index.html or so, and it returns a 404. It's because Tomcat doesn't know anything about react-redux nor javascript.

A way to handle this is to configure 404 errors to be forward to /test/index.html. It's probably how your webpack dev server is configured by default.

There is plenty of examples of doing this on apache, if you have one in front of our Tomcat. Search for html5 pushstate apache config.

Here is an example:

httpd.conf:

...
<IfModule mod_rewrite.c>
    RewriteEngine On
    RewriteBase /
    RewriteRule ^index\.html$ - [L]
    RewriteCond %{REQUEST_FILENAME} !-f
    RewriteCond %{REQUEST_FILENAME} !-d
    RewriteRule . /index.html [L]
 </IfModule>
...

If you are using Tomcat alone, you may try to specify this in the web.xml, inside your .war file:

...
<error-page>
    <error-code>404</error-code>
    <location>/index.html</location>
</error-page>
...

Note that this is not a react-router specific problem, every app that uses HTML5 pushstate() needs to handle this somehow. Javascript servers may handle this more seamlessly though.

namgold
  • 1,009
  • 1
  • 11
  • 32
Rafael Z.
  • 1,122
  • 7
  • 8
  • 1
    Thanks a lot for sheding light on this since I was totally confused. Also many thanks to @paul S for his wonderful help – Hamed Minaee Dec 20 '16 at 19:50
  • 1
    https://github.com/facebookincubator/create-react-app/blob/master/packages/react-scripts/template/README.md#serving-apps-with-client-side-routing –  Sep 04 '17 at 12:19
  • I don't know much about Apache configs but I managed to tweak this one to work seamlessly for me. The last Rewrite rule can be prepended with the name of the subfolder so that it returns the index.html in that particular folder. Thanks for the detailed explanation! – Keno Jan 18 '20 at 15:00
  • 1
    The question was about implementation in Tomcat without Apache. In such a case, the **Tomcat Rewrite** solution, presented below, is much cleaner. – Jacek J Dec 30 '21 at 08:48
  • Has anybody tested this method of deploying to Tomcat with react-router version 6.x ? I have not been able to do it. I only get a blank page without any error in the console. Any help? – Ximo Dante Sep 15 '22 at 21:57
29

Tomcat 8 and above has a simple built-in solution for this. You do not need to put it behind Apache to handle this, or switch to hash routing, or hack a redirect page to go back to a different place on your own server...

Use Tomcat Rewrite

It works for any single-page-app framework that has UI-routing like React, Angular, ExtJS, etc.


In a nutshell:

1) Add a RewriteValve to /tomcat/webapps/{your-web-app-directory}/META-INF/context.xml

<?xml version="1.0" encoding="UTF-8"?>
<Context>  
  <Valve className="org.apache.catalina.valves.rewrite.RewriteValve" />
  ... your other context items
</Context>

2) Create a file named: /tomcat/webapps/{your-web-app-directory}/WEB-INF/rewrite.config and then add rewrite rules for any paths you want to point to your index page:

RewriteRule ^/user(.*)$    /index.html [L]
RewriteRule ^/home(.*)$    /index.html [L]
RewriteRule ^/contact(.*)$ /index.html [L]

These rules tell Tomcat to send all requests with these three named paths straight to index.html.

3) Restart Tomcat

The rules can get fancier, like sending all requests to index.html except for a certain path, which is useful for /api calls that have to go somewhere besides index.html. The link above covers additional examples of how to use Tomcat Rewrite.

Jerad Rutnam
  • 1,536
  • 1
  • 14
  • 29
egerardus
  • 11,316
  • 12
  • 80
  • 123
  • Clean solution. But you need tomcat 8 above – Jerad Rutnam Jul 10 '19 at 04:05
  • Instead of writing all the routes here can we have a general signature that will redirect to index.html? – Ricky Jul 13 '20 at 12:20
  • @Ricky sure, you could do `^/(.*)$` but then you would have to add exclusions for things like favicon.ico or any other resource that your server might possibly be requested, or any api calls to get data – egerardus Jul 25 '20 at 03:16
  • Not sure why this isn't the accepted answer..it should be. The basepath thing is fine for react-router with relative contexts, but that's not what the question is. The question is about Tomcat. In case others run into this..on tomcat, I had to copy my static folder to the parent webapps folder to avoid the dreaded 'White-Screen-Of-Death' – Midiman Feb 04 '21 at 20:56
  • It is possible to write a general rewrite rule. Here is an example, assuming the app paths do not have file extensions: `RewriteRule !^/.+\.(html|css|js|jpg|png|svg|json|txt)$ /index.html [L]` – Jacek J Dec 30 '21 at 08:39
  • Saved me! thank you. One thing - when you edit `rewrite.config`, you need to re-save the `context.xml` file to take effect. (+ `META-INF/context.xml` didn't worked for me, but `$TOMCAT_DIR\conf\Catalina\localhost\WEB_APP.xml` did) – Miri Feb 16 '22 at 13:54
  • Does this solution work in reacr-rourter-dom version 6.x? @Miri, could you add more information to your experience? Thanks – Ximo Dante Sep 15 '22 at 22:13
12

I have the same problem routing not working. If 404 not redirecting and loading the index.html problem. I tried several ways and finally found a solution which fixed my problem.

This worked for me on Tomcat 8

inside ROOT folder of Tomcat make a new folder WEB-INF and create web.xml 

You can do this by

sudo gedit /opt/tomcat/webapps/ROOT/WEB-INF/web.xml

paste the below in the web.xml

<!DOCTYPE web-app PUBLIC
 "-//Sun Microsystems, Inc.//DTD Web Application 2.3//EN"
 "http://java.sun.com/dtd/web-app_2_3.dtd" >

<web-app>
    <display-name>your_display_name</display-name>

    <error-page>
        <error-code>404</error-code>
        <location>/index.html</location>
    </error-page>

</web-app>

Restart the Tomcat. This fixed the routing problem for me.

Nimantha
  • 6,405
  • 6
  • 28
  • 69
Shinto Joseph
  • 2,809
  • 27
  • 25
5

My solution is like a workaround for JSP engines (e.g. Tomcat), but it works great with minimal code.

I created an "index.jsp" parallel to "index.html" (in the content root folder) with following code:

index.jsp

<%@ page language="java" contentType="text/html; charset=UTF-8"
pageEncoding="UTF-8"%>
<%@include file="index.html"%>

I configured all urls to redirect to this JSP in web.xml

web.xml

<servlet>
    <servlet-name>index</servlet-name>
    <jsp-file>index.jsp</jsp-file>
</servlet>
<servlet-mapping>
    <servlet-name>index</servlet-name>
    <url-pattern>/*</url-pattern>
</servlet-mapping>

Now any URL requested to Tomcat will internally get redirected to index.jsp that is effectively index.html. Once react-router loads in browser, it takes care of rendering the right component and subsequent requests.

Bipul
  • 1,564
  • 1
  • 15
  • 16
3

In my case I wanted a simple solution as with creating a customer error page. However that solution still sets a status code 404 which is not desired.

So I use a small jsp file which will set status code to 200 before outputting the SPA's index.html.

I hope this helps someone else looking for a simple solution without returning status 404.

web.xml

<error-page>
    <error-code>404</error-code>
    <location>/spa.jsp</location>
</error-page>

spa.jsp

<%@ page language="java" contentType="text/html; charset=UTF-8" pageEncoding="UTF-8"%><%
    response.setStatus(200);
%><%@include file="./dashboard/index.html"%>
Clayton Bell
  • 424
  • 4
  • 4
  • This is the easiest solution and it returns a 200. Any idea if the performance on this is worse then the Tomcat 8 Rewrite solution? – bluedevil2k Jul 26 '19 at 17:37
3

For Tomcat versions below 8. You can just add a web.xml file and route the 404 to index.html.

webapps/[my-app-name]/WEB-INF/web.xml

<?xml version="1.0" encoding="UTF-8"?>
<web-app xmlns="http://java.sun.com/xml/ns/j2ee"
          xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
          xsi:schemaLocation="http://java.sun.com/xml/ns/j2ee
          http://java.sun.com/xml/ns/j2ee/web-app_2_4.xsd"
          version="2.4">

  <display-name>my-app-name</display-name>

  <error-page>
    <error-code>404</error-code>
    <location>/index.html</location>
  </error-page>

</web-app>
Jerad Rutnam
  • 1,536
  • 1
  • 14
  • 29
1

I think an answer to the OP's question should address also the access to static resources via the default servlet.

In my understanding the problem appears in multi-view React applications where a Router is used to alter the page URL in order to reflect the state of the application. This is nice, but if you reload the page by accident you get 404 since there won't be any resource matching the altered URL. It also means such altered URLs can't be bookmarked as direct access to different application views. Unless, we get a little help from the server side.

There are more solutions possible, depending on how the access to static resources is handled, or the chosen implementation (filter, servlet or even JSP), but the basic idea is to serve the main application HTML file for every Routes defined in React.

Supposing you have you have two React Routes defined in your application:

<Route path={"view1"} component={View1} />
<Route path={"view2"} component={View2} />

You could create a RouterServlet to forward requests to /view1 or /view2 back to the context root (/), supposing you mapped the application here.

void doGet(HttpServletRequest request, HttpServletResponse response) 
        throws ServletException, IOException 
{
    request.getServletContext().getRequestDispatcher("/").forward(request, response);
}

You can configure it like this:

<servlet>
    <servlet-name>RouterServlet</servlet-name>
    <servlet-class>package.RouterServlet</servlet-class>
</servlet>
<servlet-mapping>
    <servlet-name>RouterServlet</servlet-name>
    <url-pattern>/view1/*</url-pattern>
</servlet-mapping>
<servlet-mapping>
    <servlet-name>RouterServlet</servlet-name>
    <url-pattern>/view2/*</url-pattern>
</servlet-mapping>
arpadf
  • 413
  • 3
  • 13
0

While I am not familiar with Tomcat, what is most likely happening is that your server is looking for a /test/t or /test/t/index.html file, and since none exists, it is returning a 404 error.

When you use a browser history, you need to have a server that can handle routing. Typically this will be a wildcard (*) route that returns your index.html file (which in turn will return your bundled js as well as any other static files included in the index file).

One solution is to switch to using a hash router. If you are not able to do routing on the server (particularly relevant for people hosting static content), then hash routing is necessary. However, because you are using a server, you should be able to setup routing that will allow you to use a browser router.

As I said before, I am not familiar with Tomcat, so I will just describe what the configuration should be.

  1. All requests for static files should be served regularly.
  2. All other requests to /test/* (where * is any URL) should serve your index.html file.
Paul S
  • 35,097
  • 6
  • 41
  • 38
  • Hi Paul thanks for answering. The problem here is /test/t/ exists in my case and web pack dev server is able to handle it properly so I do not need to redirect it. However when I run this in tomcat web server I get the problem. – Hamed Minaee Dec 20 '16 at 18:06
  • There is a `/test/t` file on your server? – Paul S Dec 20 '16 at 18:31
  • Ahhh no there is not any actual file on server. I meant the componets such as Home and T exists. But if I do the redirection to index.html on server when a url of test/t is hit then how can I use this: This way whenever user put test/t/ he will be redirected to the index.htl and header menu not header2.js. Am I right? – Hamed Minaee Dec 20 '16 at 19:32
  • You don't want to redirect, you just want to make sure to serve the `index.html` file. On the client side, React Router will look at `window.location.pathname` and determine which route(s) to render. – Paul S Dec 20 '16 at 19:37
0

For those of you using springboot with an embedded Tomcat server the solution is fairly straightforward. I struggled a lot to reach here but hopefully it will help someone.

Its based on @rafael-z response above.

We just need to leverage the error handling provided by springboot - add the following in some Spring bean

@Bean
public ErrorPageRegistrar errorPageRegistrar() {
    return this::registerErrorPages;
}

private void registerErrorPages(ErrorPageRegistry registry) {
    registry.addErrorPages(new ErrorPage(HttpStatus.NOT_FOUND, "/index.html"));
}
anish
  • 307
  • 3
  • 14