Commit cbdcdf6f authored by Matija Obreza's avatar Matija Obreza
Browse files

Merge branch '6-ssr-404' into 'master'

Resolve "SSR 404"

Closes #6

See merge request grin-global/grin-global-ui!5
parents 4406bef6 357c2a91
......@@ -6,6 +6,7 @@
"submit": "Submit"
},
"label": {
"loadingData": null
"loadingData": null,
"notFound": "Not found"
}
}
......@@ -7,4 +7,5 @@ export default interface IRoute extends RouteProps {
key?: string;
routes?: IRoute[];
component?: any;
status?: number;
}
export default interface SsrError {
status: number;
route: string;
}
import * as React from 'react';
import { WithTranslation, withTranslation } from 'react-i18next';
interface INotFoundProps extends React.ClassAttributes<any>, WithTranslation {
}
class NotFound extends React.Component<INotFoundProps> {
public render() {
const { t } = this.props;
return (
<h3>
{ t('common:label.notFound') }
</h3>
)
}
}
export default withTranslation()(NotFound)
......@@ -3,12 +3,19 @@ import IRoute from '@gringlobal/client/model/common/IRoute';
import WelcomePage from '@gringlobal/client/ui/pages/Welcome';
import NotFound from '@gringlobal/client/ui/common/error/NotFound';
export const publicCoreRoutes: IRoute[] = [
{
component: WelcomePage,
path: '/',
exact: true,
},
{
component: NotFound,
path: '*',
status: 404,
},
] ;
......
......@@ -40,7 +40,7 @@ axiosBackend.interceptors.response.use(
window.location.replace('/');
}
return Promise.reject(error);
throw error;
},
);
......
......@@ -38,6 +38,7 @@ import sagas from 'core/action/saga';
// react-loadable webpack stats
import * as path from 'path';
import { readFileSync } from 'fs';
import { receiveSsrError } from 'core/action/serverInfo';
const reactLoadableStats = JSON.parse(readFileSync(path.join('./', 'react-loadable.json'), { encoding: 'utf8' }));
// console.log(`react-loadable stats: `, reactLoadableStats);
......@@ -66,7 +67,7 @@ const prerenderer = (html, errHtml) => (req, res) => {
req.i18n.changeLanguage(locale);
}
function renderView() {
function renderView(error?) {
// const jss = createJss(jssPreset());
const sheets = new SheetsRegistry();
const generateClassName = createGenerateClassName();
......@@ -161,19 +162,32 @@ const prerenderer = (html, errHtml) => (req, res) => {
console.log(`Rendering ${pathWithoutLang} for ${pathname}`);
const branch = matchRoutes(routes, pathWithoutLang);
if (branch && branch.length > 0) {
if (branch[branch.length - 1].route.status) {
res.status(branch[branch.length - 1].route.status);
}
}
store.dispatch(receiveLang(language));
fetchComponentData(store.dispatch, branch, search, store.getState())
.catch((err) => {
console.log('Error fetching component data', ApiError.axiosError(err));
res.status(500).end(makeErrorHtml(errHtml, { status: err.code || 500, data: err.message }, true));
// const errFilledHtml = makeErrorHtml(errHtml, err);
// res.status(500).set('Content-Type', 'text/html').send(errFilledHtml);
})
.then(() => {
return sagaReady.toPromise().then(() => {
console.log('Fetched all component data');
return renderView();
});
})
.catch((err) => {
if (+err.status === 404) {
console.log('Error while fetching component data, rendering 404 page for: ', pathWithoutLang);
store.dispatch(receiveSsrError(404, pathWithoutLang));
res.status(404);
return renderView();
}
console.log('Error fetching component data', ApiError.axiosError(err));
res.status(500).end(makeErrorHtml(errHtml, { status: err.code || 500, data: err.message }, true));
// const errFilledHtml = makeErrorHtml(errHtml, err);
// res.status(500).set('Content-Type', 'text/html').send(errFilledHtml);
}).then((html) => {
const serverRenderTime = `${Date.now() - startTime}ms`;
console.log('Server render time:', startTime, Date.now(), serverRenderTime);
......@@ -208,7 +222,7 @@ const prerenderer = (html, errHtml) => (req, res) => {
};
const makeErrorHtml = (template, error, withExplanation = false) => {
let theFilledHtml = template.replace('ERROR_MESSAGE', error.status).replace('ERROR_DETAILS', error.data);
let theFilledHtml = template.replace('ERROR_MESSAGE', error.status).replace('ERROR_DETAILS', JSON.stringify(error.data));
const explanation = 'The server is currently unavailable (because it is overloaded or down for maintenance). Generally, this is a temporary state.';
theFilledHtml = theFilledHtml.replace('ERROR_EXPLANATION', withExplanation ? explanation : '');
......
......@@ -37,10 +37,12 @@ function *appendAxiosConfig(action) {
resp = action.onSuccess(resp); // postprocess
}
yield put({ type: action.target, payload: { apiCall: ApiCall.success(resp) } });
} catch (e) {
} catch (error) {
console.log('Api error while requesting: ', error.response);
if (action.onFail) {
action.onFail(e); // postprocess
action.onFail(error); // postprocess
}
yield put({ type: action.target, payload: { apiCall: ApiCall.error(e) } });
yield put({ type: action.target, payload: { apiCall: ApiCall.error(error) } });
throw error.response;
}
}
import { RECEIVE_SSR_ERROR_INFO } from 'core/constants/serverInfo';
export const receiveSsrError = (status: number, route: string) => ({
type: RECEIVE_SSR_ERROR_INFO,
payload: {
error: { status, route },
},
});
export const RECEIVE_SSR_ERROR_INFO = 'core/serverInfo/RECEIVE_SSR_ERROR_INFO';
import { combineReducers } from 'redux';
// express reducers
import serverInfo from 'core/reducer/serverInfo';
import coreReducers from '@gringlobal/client/reducer';
// model reducers
......@@ -6,6 +8,9 @@ import cooperator from 'cooperator/reducer';
import user from 'user/reducer';
const rootReducer = (history?) => (combineReducers({
// express reducers
serverInfo,
// model reducers
cooperator,
user,
......
import update from 'immutability-helper';
// Model
import SsrError from '@gringlobal/client/model/common/SsrError';
import { RECEIVE_SSR_ERROR_INFO } from 'core/constants/serverInfo';
const INITIAL_STATE: {
ssrError: SsrError,
} = {
ssrError: null,
};
export default (state = INITIAL_STATE, action: { type?: string, payload?: any } = { type: '', payload: {} }) => {
switch (action.type) {
case RECEIVE_SSR_ERROR_INFO: {
return update(state, {
ssrError: { $set: action.payload.error },
});
}
default:
return state;
}
}
import * as React from 'react';
import { connect } from 'react-redux';
import { bindActionCreators } from 'redux';
// Model
import SsrError from '@gringlobal/client/model/common/SsrError';
// Ui
import App from '@gringlobal/client/ui/App';
import WrappedErrorApp from 'core/ui/WrappedErrorApp';
interface IExpressAppProps extends React.ClassAttributes<any> {
ssrError: SsrError;
}
class ExpressApp extends React.Component<IExpressAppProps> {
public render() {
const { ssrError } = this.props;
return ssrError ? <WrappedErrorApp { ...this.props }/> : <App { ...this.props }/>;
}
}
const mapStateToProps = (state) => ({
ssrError: state.serverInfo.ssrError,
});
const mapDispatchToProps = (dispatch) => bindActionCreators({
}, dispatch);
export default connect(mapStateToProps, mapDispatchToProps)(ExpressApp);
import * as React from 'react';
import { connect } from 'react-redux';
import { bindActionCreators } from 'redux';
import { withRouter, RouteComponentProps } from 'react-router-dom';
import renderRoutes from '@gringlobal/client/ui/renderRoutes';
// Actions
import { updateHistory } from '@gringlobal/client/action/history';
// Model
import SsrError from '@gringlobal/client/model/common/SsrError';
// Ui
import NotFound from '@gringlobal/client/ui/common/error/NotFound'
interface IWrappedErrorAppProps extends React.ClassAttributes<any>, RouteComponentProps {
ssrError: SsrError;
route?: any;
updateHistory: (location: string) => void;
}
class WrappedErrorApp extends React.Component<IWrappedErrorAppProps, any> {
public componentDidUpdate(prevProps: IWrappedErrorAppProps) {
const { updateHistory, location: prevLocation } = prevProps;
const { location } = this.props;
if (prevLocation !== null && location !== null) {
if (prevLocation !== location) {
updateHistory(`${prevLocation.pathname}${prevLocation.search ? prevLocation.search : ''}`);
}
}
}
protected getErrorPage = (status: number) => {
switch (+status) {
case 404: return NotFound;
default: return null;
}
};
protected compileErrorRoute = (ssrError) => ({
component: this.getErrorPage(ssrError.status),
path: ssrError.route,
});
public render() {
const { route: { routes }, ssrError } = this.props;
return (
<div>
{ renderRoutes([this.compileErrorRoute(ssrError), ...routes]) }
</div>
);
}
}
const mapStateToProps = (state) => ({
});
const mapDispatchToProps = (dispatch) => bindActionCreators({
updateHistory,
}, dispatch);
export default withRouter(connect(mapStateToProps, mapDispatchToProps)(WrappedErrorApp));
......@@ -11,12 +11,12 @@ import { cooperatorPublicRotes } from 'cooperator/routes';
// User
import { userPublicRotes } from 'user/routes';
import App from '@gringlobal/client/ui/App';
import ExpressApp from 'core/ui/ExpressApp';
export const routes: IRoute[] = [
{
component: App,
component: ExpressApp,
routes: [
...userPublicRotes,
...cooperatorPublicRotes,
......
Markdown is supported
0% or .
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment