-
Notifications
You must be signed in to change notification settings - Fork 54
Expand file tree
/
Copy patherrors.js
More file actions
140 lines (122 loc) · 5.12 KB
/
errors.js
File metadata and controls
140 lines (122 loc) · 5.12 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
import finalhandler from 'finalhandler';
import stream from 'stream';
import { promisify } from 'util';
import { PRODUCTION } from '../config.js';
import * as utils from '../utils/index.js';
import { Forbidden, NotFound, Unauthorized } from '../httpErrors.js';
import * as endpoints from '../endpoints/index.js';
import { AuthzDenied } from '../exceptions.js';
/* XXX TODO: Replace promisify() with require("stream/promises") once we
* upgrade to Node 15+.
* -trs, 5 Nov 2021
*/
const streamFinished = promisify(stream.finished);
const jsonMediaType = type => type.match(/^application\/(.+\+)?json$/);
function onerror(err, req, res) { // eslint-disable-line no-unused-vars
if (!err.status || err.status >= 500) {
// Only log unexpected errors
console.error(err.stack || err.toString());
}
}
/** Set up error handling. Should be done after all routes are added.
*/
export async function setup(app) {
/* Everything else gets 404ed.
*/
app.use((req, res, next) => next(new NotFound()));
/* Error handler
*/
app.useAsync(async (err, req, res, next) => { // eslint-disable-line no-unused-vars
if (res.headersSent) {
utils.verbose("Headers already sent; using custom error handler");
return finalhandler(req, res, { onerror })(err);
}
/* Read the entire request body (discarding it) if the request might have a
* body and wasn't made with Expect: 100-continue, or if it was and we wrote
* a 100 Continue response but then ended up here. This ensures that the
* request is finished being sent before we return a (error) response, which
* some clients require (such as Heroku's routing/proxy layer and the Python
* "requests" package, but notably not curl).
*/
const mayHaveBody = !["GET", "HEAD", "DELETE", "OPTIONS"].includes(req.method);
if (mayHaveBody && (!req.expectsContinue || res.wroteContinue)) {
const reqFinished = streamFinished(req);
req.unpipe();
req.resume();
await reqFinished;
}
res.vary("Accept");
/* "Is this request browser-like?" Checking for explicit inclusion of
* "text/html" is an imperfect heuristic, but still useful enough and doesn't
* require user-agent matching, which seems more fraught and more opaque.
*
* Note that we don't check req.accepts("text/html"), because that'll match
* wildcard Accept values which are sent by ~every client.
* -trs, 25 Jan 2022
*/
const isBrowserLike = req.accepts().includes("text/html");
/* Handle our authorization denied errors differently depending on if the
* request is authenticated or not and if the client is browser-like or not.
*
* The intended audience for the redirect is humans following bookmarks,
* browser history, or other saved links, which will only ever be GET (and
* _maybe_ HEAD).
*
* An additional redirect condition on navigation (vs. background request)
* would also be nice, but I can't find any good heuristic for that.
* The following seems ideal:
*
* const isNavigation = req.headers['sec-fetch-mode'] === "navigate";
*
* but it is not supported by Safari (macOS or iOS).
* -trs, 25 Jan 2022
*/
if (err instanceof AuthzDenied) {
if (!req.user) {
if (["GET", "HEAD"].includes(req.method) && isBrowserLike) {
utils.verbose(`Redirecting anonymous user to login page from ${req.originalUrl}`);
req.session.afterLoginReturnTo = req.originalUrl;
return res.redirect("/login");
}
err = new Unauthorized(err.message);
} else {
err = new Forbidden(err.message);
}
}
/* Browser-like clients get JSON if they explicitly ask for it (regardless of
* priority, and including our custom +json types) and all non-browser like
* clients get JSON.
*/
if (req.accepts().some(jsonMediaType) || !isBrowserLike) {
utils.verbose(`Sending ${err} error as JSON`);
return res.status(err.status || err.statusCode || 500)
.json({
error: err.message || String(err),
...(
!PRODUCTION
? {stack: err.stack}
: {}
),
})
.end();
}
/* Delegate to Next.js for 404s that originate from Express routes.
* Because Express routes take priority over the Next.js catch-all in
* src/routing/staticSite.js, a 404 from an Express route would not render
* with the typical site layout unless we explicitly hand it to Next.js
* here.
* Note that this is only for GET and HEAD requests to align with handling
* in src/routing/staticSite.js.
*/
if (err instanceof NotFound && ["GET", "HEAD"].includes(req.method)) {
return await endpoints.nextJsApp.handleRequest(req, res);
}
/* TODO: Serve custom error pages for user errors such as BadRequest¹ and
* Forbidden²
* ¹ <https://github.com/nextstrain/nextstrain.org/issues/774>
* ² <https://github.com/nextstrain/nextstrain.org/issues/518>
*/
utils.verbose(`Sending ${err} error as HTML with custom error handler`);
return finalhandler(req, res, { onerror })(err);
});
}