-
Notifications
You must be signed in to change notification settings - Fork 54
Expand file tree
/
Copy patherrors.js
More file actions
139 lines (123 loc) · 5.19 KB
/
errors.js
File metadata and controls
139 lines (123 loc) · 5.19 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
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$/);
/** 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) => {
/* XXX TODO: Replace calls to Express' next() with calls to our own custom
* finalhandler (the library Express uses) function configured with an
* "onerror" handler that does what we want with regard to logging.
* -trs, 1 Oct 2021
*/
if (res.headersSent) {
utils.verbose("Headers already sent; using Express' default error handler");
return next(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 only for GET and HEAD requests. This aligns with
* handling in src/routing/staticSite.js.
*/
if (err instanceof NotFound && ["GET", "HEAD"].includes(req.method)) {
/* A note about routing: if the current URL path (i.e. req.path) matches a
* a page known to the NextJS routes ("pages") then that page will be
* shown (with response code 200). Moving to server-side rendering of
* NotFound errors (and InternalServerError etc) will not only solve this
* but will also allow us to provide information about the error. See the
* following issues for more:
* <https://github.com/nextstrain/nextstrain.org/issues/774>
* <https://github.com/nextstrain/nextstrain.org/issues/518>
*/
return await endpoints.nextJsApp.handleRequest(req, res);
}
utils.verbose(`Sending ${err} error as HTML with Express' default error handler`);
if (err.status && err.status < 500) {
// Remove stack trace from user errors
err.stack = "";
}
return next(err);
});
}