Skip to content

Commit 4e57507

Browse files
committed
Auto merge of #4156 - Turbo87:crate-404, r=locks
Show 404 pages for unknown crates/versions Instead of redirecting back to a known URL and showing a notification message, we now stay on the requested URL and show an error page instead. <img width="721" alt="Bildschirmfoto 2021-11-10 um 14 41 29" src="https://user-images.githubusercontent.com/141300/141200350-d31a3b60-79a9-43eb-b104-aee1238d55f3.png"> (though with "Crate/Version not found" instead) The implementation is a little questionable, but due to the way the `error` substates in Ember.js work I've found no other way to implement this. This PR is based on #4154.
2 parents bd87c41 + b62a875 commit 4e57507

File tree

8 files changed

+128
-38
lines changed

8 files changed

+128
-38
lines changed

app/controllers/catch-all.js

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,9 @@
1+
import Controller from '@ember/controller';
2+
import { action } from '@ember/object';
3+
4+
export default class CatchAllController extends Controller {
5+
@action reload(event) {
6+
event.preventDefault();
7+
this.model.transition.retry();
8+
}
9+
}

app/routes/catch-all.js

Lines changed: 73 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,73 @@
1+
import Route from '@ember/routing/route';
2+
import { inject as service } from '@ember/service';
3+
4+
/**
5+
* This is a weird route... but let me explain.
6+
*
7+
* This is the default route that gets used if no other matching route is found.
8+
*
9+
* This route is *also* used as a generic error page via:
10+
*
11+
* ```js
12+
* this.router.replaceWith('catch-all', { transition, title: 'Something failed' });
13+
* ```
14+
*
15+
* Ideally we would use the `error` substate/routes of Ember.js, but those don't
16+
* update the URL when an error happens. This causes the native back button of the
17+
* browser to behave in strange way, so we avoid using the broken built-in error
18+
* routes.
19+
*/
20+
export default class CatchAllRoute extends Route {
21+
@service router;
22+
23+
/**
24+
* If `transitionTo('catch-all', 'foo')` is used, this hook will not get called.
25+
* If the second argument is an object, then the second object will be the `model`
26+
* of this route, and the `serialize()` hook gets called to figure out what the
27+
* URL of this route should be. The URL is automatically assembled from the passed-in
28+
* transition object.
29+
*/
30+
serialize({ transition }) {
31+
return { path: this.pathForRouteInfo(transition.to) };
32+
}
33+
34+
/**
35+
* This internal method takes a `RouteInfo` object from Ember.js (e.g. `transition.to`)
36+
* and returns the corresponding `:path` route parameter for this `catch-all` route.
37+
* @return {string}
38+
*/
39+
pathForRouteInfo(routeInfo) {
40+
let routeName = routeInfo.name;
41+
let params = paramsForRouteInfo(routeInfo);
42+
let queryParams = routeInfo.queryParams;
43+
return this.router.urlFor(routeName, ...params, { queryParams }).slice(1);
44+
}
45+
}
46+
47+
/**
48+
* Returns all route parameters for the passed-in `RouteInfo` object.
49+
*
50+
* These can be used in `router.urlFor(...)` calls.
51+
*/
52+
function paramsForRouteInfo(routeInfo) {
53+
let routeInfos = [...allRouteInfos(routeInfo)].reverse();
54+
55+
let params = [];
56+
for (let routeInfo of routeInfos) {
57+
for (let paramName of routeInfo.paramNames) {
58+
params.push(routeInfo.params[paramName]);
59+
}
60+
}
61+
return params;
62+
}
63+
64+
/**
65+
* Iterates upwards through the `RouteInfo` "family tree" until the top-most
66+
* `RouteInfo` is reached.
67+
*/
68+
function* allRouteInfos(routeInfo) {
69+
yield routeInfo;
70+
while ((routeInfo = routeInfo.parent)) {
71+
yield routeInfo;
72+
}
73+
}

app/routes/crate.js

Lines changed: 4 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -3,20 +3,18 @@ import { inject as service } from '@ember/service';
33

44
export default class CrateRoute extends Route {
55
@service headData;
6-
@service notifications;
6+
@service router;
77
@service store;
88

9-
async model(params) {
9+
async model(params, transition) {
1010
try {
1111
return await this.store.find('crate', params.crate_id);
1212
} catch (error) {
1313
if (error.errors?.some(e => e.detail === 'Not Found')) {
14-
this.notifications.error(`Crate '${params.crate_id}' does not exist`);
14+
this.router.replaceWith('catch-all', { transition, error, title: 'Crate not found' });
1515
} else {
16-
this.notifications.error(`Loading data for the '${params.crate_id}' crate failed. Please try again later!`);
16+
this.router.replaceWith('catch-all', { transition, error, title: 'Crate failed to load', tryAgain: true });
1717
}
18-
19-
this.replaceWith('index');
2018
}
2119
}
2220

app/routes/crate/version.js

Lines changed: 5 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -6,28 +6,25 @@ import { didCancel } from 'ember-concurrency';
66
import { AjaxError } from '../../utils/ajax';
77

88
export default class VersionRoute extends Route {
9-
@service notifications;
9+
@service router;
1010
@service sentry;
1111

12-
async model(params) {
12+
async model(params, transition) {
1313
let crate = this.modelFor('crate');
1414

1515
let versions;
1616
try {
1717
versions = await crate.get('versions');
18-
} catch {
19-
this.notifications.error(`Loading data for the '${crate.name}' crate failed. Please try again later!`);
20-
this.replaceWith('index');
21-
return;
18+
} catch (error) {
19+
return this.router.replaceWith('catch-all', { transition, error, title: 'Crate failed to load', tryAgain: true });
2220
}
2321

2422
let version;
2523
let requestedVersion = params.version_num;
2624
if (requestedVersion) {
2725
version = versions.find(version => version.num === requestedVersion);
2826
if (!version) {
29-
this.notifications.error(`Version '${requestedVersion}' of crate '${crate.name}' does not exist`);
30-
this.replaceWith('crate.index');
27+
return this.router.replaceWith('catch-all', { transition, title: 'Version not found' });
3128
}
3229
} else {
3330
let { defaultVersion } = crate;

app/templates/catch-all.hbs

Lines changed: 6 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -2,10 +2,12 @@
22
<div local-class="content">
33
{{svg-jar "cuddlyferris" local-class="logo"}}
44

5-
<h1 local-class="title" data-test-title>Page not found</h1>
5+
<h1 local-class="title" data-test-title>{{or @model.title "Page not found"}}</h1>
66

7-
<a href="javascript:history.back()" local-class="link">
8-
Go Back
9-
</a>
7+
{{#if @model.tryAgain}}
8+
<a href="javascript:location.reload()" local-class="link" data-test-try-again {{on "click" this.reload}}>Try Again</a>
9+
{{else}}
10+
<a href="javascript:history.back()" local-class="link" data-test-go-back>Go Back</a>
11+
{{/if}}
1012
</div>
1113
</div>

tests/acceptance/404-test.js

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,8 @@ module('Acceptance | 404', function (hooks) {
1313
assert.equal(currentURL(), '/unknown-route');
1414
assert.dom('[data-test-404-page]').exists();
1515
assert.dom('[data-test-title]').hasText('Page not found');
16+
assert.dom('[data-test-go-back]').exists();
17+
assert.dom('[data-test-try-again]').doesNotExist();
1618

1719
await percySnapshot(assert);
1820
});

tests/acceptance/crate-test.js

Lines changed: 20 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -83,18 +83,22 @@ module('Acceptance | crate page', function (hooks) {
8383

8484
test('unknown crate shows an error message', async function (assert) {
8585
await visit('/crates/nanomsg');
86-
assert.equal(currentURL(), '/');
87-
assert.dom('[data-test-notification-message]').hasText("Crate 'nanomsg' does not exist");
86+
assert.equal(currentURL(), '/crates/nanomsg');
87+
assert.dom('[data-test-404-page]').exists();
88+
assert.dom('[data-test-title]').hasText('Crate not found');
89+
assert.dom('[data-test-go-back]').exists();
90+
assert.dom('[data-test-try-again]').doesNotExist();
8891
});
8992

9093
test('other crate loading error shows an error message', async function (assert) {
9194
this.server.get('/api/v1/crates/:crate_name', {}, 500);
9295

9396
await visit('/crates/nanomsg');
94-
assert.equal(currentURL(), '/');
95-
assert
96-
.dom('[data-test-notification-message]')
97-
.hasText("Loading data for the 'nanomsg' crate failed. Please try again later!");
97+
assert.equal(currentURL(), '/crates/nanomsg');
98+
assert.dom('[data-test-404-page]').exists();
99+
assert.dom('[data-test-title]').hasText('Crate failed to load');
100+
assert.dom('[data-test-go-back]').doesNotExist();
101+
assert.dom('[data-test-try-again]').exists();
98102
});
99103

100104
test('unknown versions fall back to latest version and show an error message', async function (assert) {
@@ -104,10 +108,11 @@ module('Acceptance | crate page', function (hooks) {
104108

105109
await visit('/crates/nanomsg/0.7.0');
106110

107-
assert.equal(currentURL(), '/crates/nanomsg');
108-
assert.dom('[data-test-heading] [data-test-crate-name]').hasText('nanomsg');
109-
assert.dom('[data-test-heading] [data-test-crate-version]').hasText('0.6.1');
110-
assert.dom('[data-test-notification-message]').hasText("Version '0.7.0' of crate 'nanomsg' does not exist");
111+
assert.equal(currentURL(), '/crates/nanomsg/0.7.0');
112+
assert.dom('[data-test-404-page]').exists();
113+
assert.dom('[data-test-title]').hasText('Version not found');
114+
assert.dom('[data-test-go-back]').exists();
115+
assert.dom('[data-test-try-again]').doesNotExist();
111116
});
112117

113118
test('other versions loading error shows an error message', async function (assert) {
@@ -119,10 +124,11 @@ module('Acceptance | crate page', function (hooks) {
119124

120125
await visit('/');
121126
await click('[data-test-just-updated] [data-test-crate-link="0"]');
122-
assert.equal(currentURL(), '/');
123-
assert
124-
.dom('[data-test-notification-message]')
125-
.hasText("Loading data for the 'nanomsg' crate failed. Please try again later!");
127+
assert.equal(currentURL(), '/crates/nanomsg');
128+
assert.dom('[data-test-404-page]').exists();
129+
assert.dom('[data-test-title]').hasText('Crate failed to load');
130+
assert.dom('[data-test-go-back]').doesNotExist();
131+
assert.dom('[data-test-try-again]').exists();
126132
});
127133

128134
test('navigating to the all versions page', async function (assert) {

tests/routes/crate/version/model-test.js

Lines changed: 9 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,10 @@
1-
import { currentURL, visit } from '@ember/test-helpers';
1+
import { currentURL } from '@ember/test-helpers';
22
import { module, test } from 'qunit';
33

44
import { setupApplicationTest } from 'cargo/tests/helpers';
55

6+
import { visit } from '../../../helpers/visit-ignoring-abort';
7+
68
module('Route | crate.version | model() hook', function (hooks) {
79
setupApplicationTest(hooks);
810

@@ -20,17 +22,18 @@ module('Route | crate.version | model() hook', function (hooks) {
2022
assert.dom('[data-test-notification-message]').doesNotExist();
2123
});
2224

23-
test('redirects to unspecific version URL', async function (assert) {
25+
test('shows error page for unknown versions', async function (assert) {
2426
let crate = this.server.create('crate', { name: 'foo' });
2527
this.server.create('version', { crate, num: '1.0.0' });
2628
this.server.create('version', { crate, num: '1.2.3', yanked: true });
2729
this.server.create('version', { crate, num: '2.0.0-beta.1' });
2830

2931
await visit('/crates/foo/2.0.0');
30-
assert.equal(currentURL(), `/crates/foo`);
31-
assert.dom('[data-test-crate-name]').hasText('foo');
32-
assert.dom('[data-test-crate-version]').hasText('1.0.0');
33-
assert.dom('[data-test-notification-message="error"]').hasText("Version '2.0.0' of crate 'foo' does not exist");
32+
assert.equal(currentURL(), `/crates/foo/2.0.0`);
33+
assert.dom('[data-test-404-page]').exists();
34+
assert.dom('[data-test-title]').hasText('Version not found');
35+
assert.dom('[data-test-go-back]').exists();
36+
assert.dom('[data-test-try-again]').doesNotExist();
3437
});
3538
});
3639

0 commit comments

Comments
 (0)