-
-
Notifications
You must be signed in to change notification settings - Fork 2k
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Error handling fix #5314
Error handling fix #5314
Changes from all commits
c9c152e
48ddd8c
5ecfc93
bfa3dec
f72ddd1
adaf85e
ec052ea
2e41c78
9d0fbcf
5216d42
f7f9fc2
1852bbe
618e16e
e4f886b
30fd42f
ec1cdde
bb1c710
3c8360e
3a2127f
c8b9feb
2694afa
75e69e1
3940383
091c09f
91aa2dd
01fdc7e
fcbb899
ca762d8
b19c3f5
402ae5c
2458f3a
83c6252
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,6 @@ | ||
--- | ||
'@sveltejs/kit': patch | ||
'test-basics': patch | ||
--- | ||
|
||
Returns errors from page endpoints as JSON where appropriate |
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -55,3 +55,44 @@ export function normalize_request_method(event) { | |
const method = event.request.method.toLowerCase(); | ||
return method === 'delete' ? 'del' : method; // 'delete' is a reserved word | ||
} | ||
|
||
/** | ||
* Serialize an error into a JSON string, by copying its `name`, `message` | ||
* and (in dev) `stack`, plus any custom properties, plus recursively | ||
* serialized `cause` properties. This is necessary because | ||
* `JSON.stringify(error) === '{}'` | ||
* @param {Error} error | ||
* @param {(error: Error) => string | undefined} get_stack | ||
*/ | ||
export function serialize_error(error, get_stack) { | ||
return JSON.stringify(clone_error(error, get_stack)); | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. it could use a comment explaining why we're doing a clone There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. added |
||
} | ||
|
||
/** | ||
* @param {Error} error | ||
* @param {(error: Error) => string | undefined} get_stack | ||
*/ | ||
function clone_error(error, get_stack) { | ||
const { | ||
benmccann marked this conversation as resolved.
Show resolved
Hide resolved
|
||
name, | ||
message, | ||
// this should constitute 'using' a var, since it affects `custom` | ||
// eslint-disable-next-line | ||
stack, | ||
// @ts-expect-error i guess typescript doesn't know about error.cause yet | ||
cause, | ||
...custom | ||
benmccann marked this conversation as resolved.
Show resolved
Hide resolved
|
||
} = error; | ||
|
||
/** @type {Record<string, any>} */ | ||
const object = { name, message, stack: get_stack(error) }; | ||
|
||
if (cause) object.cause = clone_error(cause, get_stack); | ||
|
||
for (const key in custom) { | ||
// @ts-expect-error | ||
object[key] = custom[key]; | ||
} | ||
|
||
return object; | ||
} |
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -19,3 +19,57 @@ export function to_headers(object) { | |
|
||
return headers; | ||
} | ||
|
||
/** | ||
* Given an Accept header and a list of possible content types, pick | ||
* the most suitable one to respond with | ||
* @param {string} accept | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. might not hurt to add descriptions for these two. I guess it's the accept header and types that we're okay using or something? There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. added |
||
* @param {string[]} types | ||
*/ | ||
export function negotiate(accept, types) { | ||
const parts = accept | ||
.split(',') | ||
.map((str, i) => { | ||
const match = /([^/]+)\/([^;]+)(?:;q=([0-9.]+))?/.exec(str); | ||
if (match) { | ||
const [, type, subtype, q = '1'] = match; | ||
return { type, subtype, q: +q, i }; | ||
} | ||
|
||
throw new Error(`Invalid Accept header: ${accept}`); | ||
}) | ||
.sort((a, b) => { | ||
if (a.q !== b.q) { | ||
return b.q - a.q; | ||
} | ||
|
||
if ((a.subtype === '*') !== (b.subtype === '*')) { | ||
return a.subtype === '*' ? 1 : -1; | ||
} | ||
|
||
if ((a.type === '*') !== (b.type === '*')) { | ||
return a.type === '*' ? 1 : -1; | ||
} | ||
|
||
return a.i - b.i; | ||
}); | ||
|
||
let accepted; | ||
let min_priority = Infinity; | ||
|
||
for (const mimetype of types) { | ||
const [type, subtype] = mimetype.split('/'); | ||
const priority = parts.findIndex( | ||
(part) => | ||
(part.type === type || part.type === '*') && | ||
(part.subtype === subtype || part.subtype === '*') | ||
); | ||
|
||
if (priority !== -1 && priority < min_priority) { | ||
accepted = mimetype; | ||
min_priority = priority; | ||
} | ||
} | ||
|
||
return accepted; | ||
} |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,16 @@ | ||
<script> | ||
import { page } from '$app/stores'; | ||
</script> | ||
|
||
<pre>{JSON.stringify( | ||
{ | ||
status: $page.status, | ||
name: $page.error.name, | ||
message: $page.error.message, | ||
stack: $page.error.stack, | ||
// @ts-expect-error | ||
fancy: $page.error.fancy | ||
}, | ||
null, | ||
' ' | ||
)}</pre> |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,8 @@ | ||
export class FancyError extends Error { | ||
name = 'FancyError'; | ||
fancy = true; | ||
|
||
constructor(message, options) { | ||
super(message, options); | ||
} | ||
} |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,6 @@ | ||
import { FancyError } from './_shared.js'; | ||
|
||
export const get = () => ({ | ||
status: 400, | ||
body: new FancyError('oops') | ||
}); |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1 @@ | ||
<h1>if you see this something went wrong</h1> |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,5 @@ | ||
import { FancyError } from './_shared.js'; | ||
|
||
export const get = () => { | ||
throw new FancyError('oops'); | ||
}; |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1 @@ | ||
<h1>if you see this something went wrong</h1> |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,10 @@ | ||
<a id="get-implicit" href="/errors/page-endpoint/get-implicit">GET (implicit)</a> | ||
<a id="get-explicit" href="/errors/page-endpoint/get-explicit">GET (explicit)</a> | ||
|
||
<form action="/errors/page-endpoint/post-implicit" method="post"> | ||
<button type="submit" id="post-implicit">POST (implicit)</button> | ||
</form> | ||
|
||
<form action="/errors/page-endpoint/post-explicit" method="post"> | ||
<button type="submit" id="post-explicit">POST (explicit)</button> | ||
</form> |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,6 @@ | ||
import { FancyError } from './_shared.js'; | ||
|
||
export const post = () => ({ | ||
status: 400, | ||
body: new FancyError('oops') | ||
}); |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1 @@ | ||
<h1>if you see this something went wrong</h1> |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,5 @@ | ||
import { FancyError } from './_shared.js'; | ||
|
||
export const post = () => { | ||
throw new FancyError('oops'); | ||
}; |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1 @@ | ||
<h1>if you see this something went wrong</h1> |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
could refactor this into a reusable function since the other new code added here does the same thing
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
feels like unnecessary indirection to me — you add function call overhead, a new utility somewhere in the codebase, an import declaration per consuming module... you shave off a few characters at the callsite but make the codebase larger and arguably less easy to read
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
ah, you're talking about the whole block, not just the highlighted line. thought you mean
is_error(body)
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
this is one of those cases where you basically can't DRY it out, because of the control flow (i.e. mutating an existing object and conditionally returning it). Or rather you can, but the resulting function is larger than the code you saved by deduplicating. i reckon it's probably not worth it