Skip to content

Commit 50d4d38

Browse files
committed
feat(catch-all): handled mapping the catch-all route to a 404 status
1 parent c48d04b commit 50d4d38

File tree

12 files changed

+70
-22
lines changed

12 files changed

+70
-22
lines changed

README.md

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -45,6 +45,7 @@ export default {
4545
<IndexRoute component={Index}/>
4646
<Route path="/foo" component={Foo}/>
4747
<Route path="/bar" component={Bar}/>
48+
<Route path="*" component={NotFound}/>
4849
</Route>
4950
),
5051
Root: ({store, children}) => (
@@ -69,6 +70,10 @@ are currently not provided, so these dependencies are required.
6970
additional steps before the response
7071
* `routes`: the definition of your react-router routes that this plugin should match the request url
7172
against
73+
* If you use a [catch-all route](https://github.com/ReactTraining/react-router/blob/c3cd9675bd8a31368f87da74ac588981cbd6eae7/upgrade-guides/v1.0.0.md#notfound-route)
74+
to display an appropriate message when the route does not match, it should have a `displayName` of `NotFound`. This
75+
will enable the status code to be passed to `respond` as `404`. Please note that the automatic mapping of the `name`
76+
property should not be relied on because it can be mangled during minification and, therefore, not match in production.
7277
* `Root`: a react component that will wrap the mounted components that result from the matched route
7378
* `store`: a data store that will be passed as a prop to the `<Root />` component so that your
7479
component can inject it into the context through a provider component.

example/components/not-found.js

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,12 @@
1+
import React from 'react';
2+
3+
export default function NotFound() {
4+
return (
5+
<div>
6+
<h1>404</h1>
7+
<p>Page Not Found</p>
8+
</div>
9+
);
10+
}
11+
12+
NotFound.displayName = 'NotFound';

example/manifest.js

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -41,7 +41,7 @@ export default {
4141
{
4242
plugin: {
4343
register: '../src/route',
44-
options: {respond, routes, Root, store: createStore(() => undefined)}
44+
options: {respond, routes, Root, configureStore: () => createStore(() => undefined)}
4545
}
4646
}
4747
]

example/respond.js

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
1-
export default function respond(reply, {renderedContent}) {
1+
export default function respond(reply, {renderedContent, status}) {
22
reply.view('layout', {
33
renderedContent,
44
title: '<title>Example Title</title>'
5-
});
5+
}).code(status);
66
}

example/routes.js

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -5,13 +5,13 @@ import Wrap from './components/wrap';
55
import Index from './components/index';
66
import Foo from './components/foo';
77
import Bar from './components/bar';
8+
import NotFound from './components/not-found';
89

9-
const routes = (
10+
export default (
1011
<Route path="/" component={Wrap}>
1112
<IndexRoute component={Index} />
1213
<Route path="/foo" component={Foo} />
1314
<Route path="/bar" component={Bar} />
15+
<Route path="*" component={NotFound} />
1416
</Route>
1517
);
16-
17-
export default routes;

package.json

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -94,5 +94,8 @@
9494
"commitizen": {
9595
"path": "./node_modules/cz-conventional-changelog"
9696
}
97+
},
98+
"dependencies": {
99+
"http-status-codes": "1.0.6"
97100
}
98101
}

src/data-fetcher.js

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,9 @@
11
import {trigger} from 'redial';
22

3-
export default function ({renderProps, store}) {
3+
export default function ({renderProps, store, status}) {
44
return trigger('fetch', renderProps.components, {
55
params: renderProps.params,
66
dispatch: store.dispatch,
77
state: store.getState()
8-
}).then(() => Promise.resolve(({renderProps}))).catch(e => Promise.reject(e));
8+
}).then(() => Promise.resolve(({renderProps, status}))).catch(e => Promise.reject(e));
99
}

src/route-matcher.js

Lines changed: 8 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,12 @@
1+
import {OK, NOT_FOUND} from 'http-status-codes';
12
import {match, createMemoryHistory} from 'react-router';
23

4+
function determineStatusFrom(components) {
5+
if (components.map(component => component.displayName).includes('NotFound')) return NOT_FOUND;
6+
7+
return OK;
8+
}
9+
310
export default function matchRoute(url, routes) {
411
return new Promise((resolve, reject) => {
512
const history = createMemoryHistory();
@@ -9,7 +16,7 @@ export default function matchRoute(url, routes) {
916
reject(err);
1017
}
1118

12-
resolve({redirectLocation, renderProps});
19+
resolve({redirectLocation, renderProps, status: determineStatusFrom(renderProps.components)});
1320
});
1421
});
1522
}

src/router-wrapper.js

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -7,9 +7,10 @@ import fetchData from './data-fetcher';
77

88
export default function renderThroughReactRouter(request, reply, {routes, respond, Root, store}) {
99
return matchRoute(request.raw.req.url, routes)
10-
.then(({renderProps}) => fetchData({renderProps, store}))
11-
.then(({renderProps}) => respond(reply, {
10+
.then(({renderProps, status}) => fetchData({renderProps, store, status}))
11+
.then(({renderProps, status}) => respond(reply, {
1212
store,
13+
status,
1314
renderedContent: renderToString(
1415
<Root request={request} store={store}>
1516
<RouterContext {...renderProps} />

test/unit/data-fetcher-test.js

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -22,10 +22,11 @@ suite('data fetcher', () => {
2222
const state = any.simpleObject();
2323
const getState = sinon.stub().returns(state);
2424
const renderProps = {...any.simpleObject(), components, params};
25+
const status = any.integer();
2526
const store = {...any.simpleObject(), dispatch, getState};
2627
redial.trigger.withArgs('fetch', components, {params, dispatch, state}).resolves();
2728

28-
return assert.isFulfilled(fetchData({renderProps, store}), {renderProps});
29+
return assert.isFulfilled(fetchData({renderProps, store, status}), {renderProps, status});
2930
});
3031

3132
test('that a redial rejection bubbles', () => {

test/unit/route-matcher-test.js

Lines changed: 25 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,4 @@
1+
import {OK, NOT_FOUND} from 'http-status-codes';
12
import * as reactRouter from 'react-router';
23
import sinon from 'sinon';
34
import {assert} from 'chai';
@@ -7,6 +8,10 @@ import matchRoute from '../../src/route-matcher';
78
suite('route matcher', () => {
89
let sandbox;
910
const createLocation = sinon.stub();
11+
const routes = any.simpleObject();
12+
const redirectLocation = any.string();
13+
const renderProps = any.simpleObject();
14+
const url = any.string();
1015

1116
setup(() => {
1217
sandbox = sinon.sandbox.create();
@@ -21,15 +26,16 @@ suite('route matcher', () => {
2126
});
2227

2328
test('that renderProps and redirectLocation are returned when matching resolves', () => {
24-
const url = any.string();
2529
const location = any.string();
26-
const routes = any.simpleObject();
27-
const renderProps = any.simpleObject();
28-
const redirectLocation = any.string();
2930
createLocation.withArgs(url).returns(location);
30-
reactRouter.match.withArgs({location, routes}).yields(null, redirectLocation, renderProps);
31-
32-
return assert.becomes(matchRoute(url, routes), {redirectLocation, renderProps});
31+
const renderPropWithComponents = {...renderProps, components: any.listOf(() => ({displayName: any.word()}))};
32+
reactRouter.match.withArgs({location, routes}).yields(null, redirectLocation, renderPropWithComponents);
33+
34+
return assert.becomes(matchRoute(url, routes), {
35+
redirectLocation,
36+
renderProps: renderPropWithComponents,
37+
status: OK
38+
});
3339
});
3440

3541
test('that a matching error results in a rejection', () => {
@@ -38,4 +44,16 @@ suite('route matcher', () => {
3844

3945
return assert.isRejected(matchRoute(), error);
4046
});
47+
48+
test('that the status code is returned as 404 when the catch-all route matches', () => {
49+
const components = [{displayName: any.string()}, {displayName: 'NotFound'}, {displayName: any.string()}];
50+
const renderPropsWithComponents = {components};
51+
reactRouter.match.yields(null, redirectLocation, renderPropsWithComponents);
52+
53+
return assert.becomes(matchRoute(url, routes), {
54+
redirectLocation,
55+
renderProps: renderPropsWithComponents,
56+
status: NOT_FOUND
57+
});
58+
});
4159
});

test/unit/router-wrapper-test.js

Lines changed: 4 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -31,20 +31,21 @@ suite('router-wrapper', () => {
3131
const request = {raw: {req: {url}}};
3232
const reply = sinon.spy();
3333
const renderProps = any.simpleObject();
34+
const status = any.integer();
3435
const context = any.simpleObject();
3536
const Root = any.simpleObject();
3637
const store = any.simpleObject();
3738
const rootComponent = any.simpleObject();
3839
const renderedContent = any.string();
39-
routeMatcher.default.withArgs(url, routes).resolves({renderProps});
40-
dataFetcher.default.withArgs({renderProps, store}).resolves({renderProps});
40+
routeMatcher.default.withArgs(url, routes).resolves({renderProps, status});
41+
dataFetcher.default.withArgs({renderProps, store, status}).resolves({renderProps, status});
4142
React.createElement.withArgs(RouterContext, sinon.match(renderProps)).returns(context);
4243
React.createElement.withArgs(Root, {request, store}).returns(rootComponent);
4344
domServer.renderToString.withArgs(rootComponent).returns(renderedContent);
4445

4546
return renderThroughReactRouter(request, reply, {routes, respond, Root, store}).then(() => {
4647
assert.notCalled(reply);
47-
assert.calledWith(respond, reply, {renderedContent, store});
48+
assert.calledWith(respond, reply, {renderedContent, store, status});
4849
});
4950
});
5051

0 commit comments

Comments
 (0)