ANSWER for now
This was tough for me to get exactly right. Very little in the way of guidance via Google. I hope this helps others.
As Dan Cornilescu pointed out, the handlers accept the first match. So we go from more specific to less specific. I've solved this by following the folder structure created by npm run build
:
1. Handle sub-directories for js, css, and media.
2. Handle json and ico requests for manifest.json and fav.ico.
3. Route all other traffic to index.html.
handlers:
- url: /static/js/(.*)
static_files: build/static/js/\1
upload: build/static/js/(.*)
- url: /static/css/(.*)
static_files: build/static/css/\1
upload: build/static/css/(.*)
- url: /static/media/(.*)
static_files: build/static/media/\1
upload: build/static/media/(.*)
- url: /(.*\.(json|ico))$
static_files: build/\1
upload: build/.*\.(json|ico)$
- url: /
static_files: build/index.html
upload: build/index.html
- url: /.*
static_files: build/index.html
upload: build/index.html
More efficient answers welcome.
Original Question:
Setting GAE app.yaml for react-router routes produces "Unexpected token <" errors.
In the development enviroment, all routes work when called directly. localhost:5000 and localhost:5000/test produce expected results.
In GAE standard app.yaml functions for the root directory when the URL www.test-app.com is called directly. www.test-app.com/test produces a 404 error.
app.yaml #1
runtime: nodejs8
instance_class: F1
automatic_scaling:
max_instances: 1
handlers:
- url: /
static_files: build/index.html
upload: build/index.html
Configuring app.yaml to accept wildcard routes fails for all paths. www.test-app.com/ and www.test-app.com/test produce an error "Unexpected token <". It appears that it is serving .js files as index.html.
app.yaml #2
runtime: nodejs8
instance_class: F1
automatic_scaling:
max_instances: 1
handlers:
- url: /.*
static_files: build/index.html
upload: build/index.html
Steps to reproduce this issue:
Node: 10.15.0 npm: 6.4.1
gcloud init
via Google Cloud SDK
npm init react-app test-app
npm install react-router-dom
Add router to index.js:
index.js
import {BrowserRouter as Router, Route} from 'react-router-dom';
import React from 'react';
import ReactDOM from 'react-dom';
import './index.css';
import App from './App';
import * as serviceWorker from './serviceWorker';
ReactDOM.render(
<Router>
<App />
</Router>,
document.getElementById('root'));
serviceWorker.unregister();
Add routing to app.js:
app.js
import {Route} from 'react-router-dom'
import React, { Component } from 'react';
import logo from './logo.svg';
import './App.css';
class App extends Component {
render() {
return (
<div>
<Route exact path="/"
render={() => <div className="App">
<header className="App-header">
<img src={logo} className="App-logo" alt="logo" />
<p>
Edit <code>src/App.js</code> and save to reload.
</p>
<a
className="App-link"
href="https://reactjs.org"
target="_blank"
rel="noopener noreferrer"
>
Learn React
</a>
</header>
</div>} />
<Route exact path="/test"
render={() => <div className="App">
Hello, World!
</div>} />
</div>
);
}
}
export default App;
No changes to package.json:
package.json
{
"name": "test-app",
"version": "0.1.0",
"private": true,
"dependencies": {
"react": "^16.7.0",
"react-dom": "^16.7.0",
"react-router-dom": "^4.3.1",
"react-scripts": "2.1.3"
},
"scripts": {
"start": "react-scripts start",
"build": "react-scripts build",
"test": "react-scripts test",
"eject": "react-scripts eject"
},
"eslintConfig": {
"extends": "react-app"
},
"browserslist": [
">0.2%",
"not dead",
"not ie <= 11",
"not op_mini all"
]
}
npm run build
gcloud app deploy
When a request comes in its URL path is compared against the patterns specified in the
handlers
section of theapp.yaml
file, in the order in which they are specified. The first match wins and whatever the matching handler specifies is executed. Once you grasp this there's no more need to guess what's going on.So you're making requests to
www.test-app.com
andwww.test-app.com/test
, which translate to the/
and/test
URL paths, respectively.With your 1st
app.yaml
(note that your patterns are identical in it, the 2nd one will never be matched):/
matches the 1st pattern, the staticbuild/index.html
from your app directory (not a .js file!) will be returned/test
doesn't match any pattern, 404 will be returnedWith your 2nd
app.yaml
:/
matches the 2nd pattern, the staticbuild
dir will be served (which I presume triggers your node app, but I'm not certain - I'm not a node user)/test
matches the 1st pattern, again that staticbuild/index.html
file will be returnedI suspect the
build/index.html
file from your app directory from where you made your deployment (that entirebuild
dir was uploaded as a static dir) had HTML syntax errors at the time when you made the deployment (that's how it was frozen - it's static), causing the error message you see.You might have fixed the local copy of the file since the deployment, which could explain why it appears to be working locally now. If so a re-deployment of the app should fix the problems on GAE as well.
UPDATE:
I don't think you'd want a static handler for your
build/index.html
- it'll always serve whatever content was in that file in your local workspace at deployment time.I'd follow the non-static example from the official app.yaml Configuration File (updated with a
.*
pattern instead of the/.*
original one to be certain it matches/
as well):It might even be possible to drop the
handlers
section altogether, I don't see one in several Node.js samples repo examples:I took the answers from Nathan Shephard and Dan Cornilescu and condensed them into the app.yaml below. It seems to be working for my React SPA on GAE Standard. No application server (ex: serve.js, ExpressJS, etc) is necessary.