Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
7 changes: 6 additions & 1 deletion packages/kit/src/runtime/client/client.js
Original file line number Diff line number Diff line change
Expand Up @@ -652,7 +652,12 @@ export function create_client({ target, session, base, trailing_slash }) {
}
} else {
status = res.status;
error = new Error('Failed to load data');

try {
error = await res.json();
} catch (e) {
error = new Error('Failed to load data');
}
}
}

Expand Down
5 changes: 4 additions & 1 deletion packages/kit/src/runtime/server/endpoint.js
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import { to_headers } from '../../utils/http.js';
import { enumerate_error_props, stringify_safe } from '../../utils/object.js';
import { hash } from '../hash.js';
import { is_pojo, normalize_request_method } from './utils.js';

Expand Down Expand Up @@ -82,7 +83,9 @@ export async function render_endpoint(event, mod) {

if (is_pojo(body) && (!type || type.startsWith('application/json'))) {
headers.set('content-type', 'application/json; charset=utf-8');
normalized_body = JSON.stringify(body);
normalized_body = stringify_safe(body);
} else if (body instanceof Error) {
normalized_body = stringify_safe(enumerate_error_props(body));
} else {
normalized_body = /** @type {import('types').StrictBody} */ (body);
}
Expand Down
39 changes: 10 additions & 29 deletions packages/kit/src/runtime/server/page/load_node.js
Original file line number Diff line number Diff line change
Expand Up @@ -398,14 +398,13 @@ async function load_shadow_data(route, event, options, prerender) {
body: {}
};

if (!is_get) {
if (handler) {
const result = await handler(event);

if (result.fallthrough) return result;

const { status, headers, body } = validate_shadow_output(result);
data.status = status;

add_cookies(/** @type {string[]} */ (data.cookies), headers);

// Redirects are respected...
Expand All @@ -416,35 +415,17 @@ async function load_shadow_data(route, event, options, prerender) {
return data;
}

// ...but 4xx and 5xx status codes _don't_ result in the error page
// rendering for non-GET requests — instead, we allow the page
// to render with any validation errors etc that were returned
data.body = body;
}

const get = (method === 'head' && mod.head) || mod.get;
if (get) {
const result = await get(event);

if (result.fallthrough) return result;

const { status, headers, body } = validate_shadow_output(result);
add_cookies(/** @type {string[]} */ (data.cookies), headers);
data.status = status;

if (status >= 400) {
data.error = new Error('Failed to load data');
return data;
}

if (status >= 300) {
data.redirect = /** @type {string} */ (
headers instanceof Headers ? headers.get('location') : headers.location
);
// ...errors are propagated to the page...
if (status >= 400 && body instanceof Error) {
data.error = body;
return data;
}

data.body = { ...body, ...data.body };
// ...but 4xx and 5xx status codes (if they are accompanied by a
// non-Error body) _don't_ result in the error page rendering —
// instead, we allow the page to render with any validation errors
// etc that were returned
data.body = body;
}

return data;
Expand Down Expand Up @@ -491,7 +472,7 @@ function validate_shadow_output(result) {
headers = lowercase_keys(/** @type {Record<string, string>} */ (headers));
}

if (!is_pojo(body)) {
if (!is_pojo(body) && !(body instanceof Error)) {
throw new Error('Body returned from endpoint request handler must be a plain object');
}

Expand Down
2 changes: 2 additions & 0 deletions packages/kit/src/runtime/server/page/render.js
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import devalue from 'devalue';
import { readable, writable } from 'svelte/store';
import { coalesce_to_error } from '../../../utils/error.js';
import { enumerate_error_props } from '../../../utils/object.js';
import { hash } from '../../hash.js';
import { render_json_payload_script } from '../../../utils/escape.js';
import { s } from '../../../utils/misc.js';
Expand Down Expand Up @@ -345,6 +346,7 @@ function try_serialize(data, fail) {
/** @param {(Error & {frame?: string} & {loc?: object}) | undefined | null} error */
function serialize_error(error) {
if (!error) return null;
error = enumerate_error_props(error);
let serialized = try_serialize(error);
if (!serialized) {
const { name, message, stack } = error;
Expand Down
1 change: 1 addition & 0 deletions packages/kit/src/runtime/server/utils.js
Original file line number Diff line number Diff line change
Expand Up @@ -38,6 +38,7 @@ export function is_pojo(body) {

if (body) {
if (body instanceof Uint8Array) return false;
if (body instanceof Error) return false;

// body could be a node Readable, but we don't want to import
// node built-ins, so we use duck typing
Expand Down
67 changes: 67 additions & 0 deletions packages/kit/src/utils/object.js
Original file line number Diff line number Diff line change
Expand Up @@ -94,3 +94,70 @@ function merge_into(a, b, conflicts = [], path = []) {
}
}
}

export function enumerate_error_props(error) {
const seen = new Map();
const replacements = new Set();

function loop(error) {
const { name, message, stack } = error;
const obj = { ...error, name, message, stack };

for (const [key, val] of Object.entries(obj)) {
if (val instanceof Error) {
if (seen.has(val)) {
// Store a reference so the error can be replaced with an
// enumerated object once recursion is complete.
replacements.add({ obj, key, error: val });
} else {
// Set a placeholder to prevent infinite recursion.
seen.set(val, 'placeholder');

// Recurse into the error.
const enumerated = loop(val);

// Overwrite the placeholder with the enumerated error object.
seen.set(val, enumerated);
obj[key] = enumerated;
}
}
}

return obj;
}

const output = loop(error);

for (const item of replacements) {
const { obj, key, error } = item;
const enumerated = seen.get(error);
if (enumerated) {
obj[key] = enumerated;
}
}

return output;
}

// Something like https://github.com/moll/json-stringify-safe. Similar to
// devalue, but intentionaly *breaks* circular references so the object can be
// serialized to JSON
export function stringify_safe(obj) {
const stack = [];
const keys = [];
const cycleReplacer = function (key, value) {
if (stack[0] === value) return '[Circular ~]';
return '[Circular ~.' + keys.slice(0, stack.indexOf(value)).join('.') + ']';
};

return JSON.stringify(obj, function (key, value) {
if (stack.length > 0) {
const thisPos = stack.indexOf(this);
~thisPos ? stack.splice(thisPos + 1) : stack.push(this);
~thisPos ? keys.splice(thisPos, Infinity, key) : keys.push(key);
if (~stack.indexOf(value)) value = cycleReplacer.call(this, key, value);
} else stack.push(value);

return value;
});
}
Original file line number Diff line number Diff line change
@@ -1,5 +1,11 @@
export function get() {
const cause = new Error('cause');
const error = new Error('show yourself, coward');
error.cause = cause;
error.circular = error;

return {
status: 404
status: 503,
body: error
};
}