Skip to content

Commit 3f6864a

Browse files
committed
[breaking] use devalue to (de)serialize action data
Allows for Date objects and more be part of the response Closes #7488
1 parent 3c25c07 commit 3f6864a

File tree

4 files changed

+43
-42
lines changed

4 files changed

+43
-42
lines changed

.changeset/cool-emus-drive.md

+5
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
---
2+
'@sveltejs/kit': patch
3+
---
4+
5+
[breaking] use devalue to (de)serialize action data

packages/kit/src/runtime/app/forms.js

+5-1
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
1-
import { invalidateAll } from './navigation.js';
1+
import * as devalue from 'devalue';
22
import { client } from '../client/singletons.js';
3+
import { invalidateAll } from './navigation.js';
34

45
/**
56
* @param {string} name
@@ -92,6 +93,9 @@ export function enhance(form, submit = () => {}) {
9293
});
9394

9495
result = await response.json();
96+
if (result.data) {
97+
result.data = devalue.parse(result.data);
98+
}
9599
} catch (error) {
96100
if (/** @type {any} */ (error)?.name === 'AbortError') return;
97101
result = { type: 'error', error };

packages/kit/src/runtime/server/page/actions.js

+31-39
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,4 @@
1+
import * as devalue from 'devalue';
12
import { error, json } from '../../../exports/index.js';
23
import { normalize_error } from '../../../utils/error.js';
34
import { is_form_content_type, negotiate } from '../../../utils/http.js';
@@ -41,14 +42,16 @@ export async function handle_action_json_request(event, options, server) {
4142
const data = await call_action(event, actions);
4243

4344
if (data instanceof ValidationError) {
44-
check_serializability(data.data, /** @type {string} */ (event.routeId), 'data');
45-
return action_json({ type: 'invalid', status: data.status, data: data.data });
45+
return action_json({
46+
type: 'invalid',
47+
status: data.status,
48+
data: uneval(data.data, /** @type {string} */ (event.routeId))
49+
});
4650
} else {
47-
check_serializability(data, /** @type {string} */ (event.routeId), 'data');
4851
return action_json({
4952
type: 'success',
5053
status: data ? 200 : 204,
51-
data: /** @type {Record<string, any> | undefined} */ (data)
54+
data: uneval(data, /** @type {string} */ (event.routeId))
5255
});
5356
}
5457
} catch (e) {
@@ -211,46 +214,35 @@ function maybe_throw_migration_error(server) {
211214
}
212215

213216
/**
214-
* Check that the data can safely be serialized to JSON
215-
* @param {any} value
216-
* @param {string} id
217-
* @param {string} path
217+
* Try to `devalue.stringify` the data object, and if it fails, return a proper Error with context
218+
* @param {any} data
219+
* @param {string} routeId
218220
*/
219-
function check_serializability(value, id, path) {
220-
const type = typeof value;
221+
export function stringify_action_response(data, routeId) {
222+
try {
223+
return devalue.stringify(data);
224+
} catch (e) {
225+
// If we're here, the data could not be serialized with devalue
226+
const error = /** @type {any} */ (e);
227+
const match = /\[(\d+)\]\.data\.(.+)/.exec(error.path);
228+
if (match) {
229+
throw new Error(
230+
`Data returned from \`action\` inside ${routeId} is not serializable: ${error.message} (data.${match[2]})`
231+
);
232+
}
221233

222-
if (type === 'string' || type === 'boolean' || type === 'number' || type === 'undefined') {
223-
// primitives are fine
224-
return;
225-
}
234+
const nonPojoError = /pojo/i.exec(error.message);
226235

227-
if (type === 'object') {
228-
// nulls are fine...
229-
if (!value) return;
236+
if (nonPojoError) {
237+
const constructorName = data?.constructor?.name;
230238

231-
// ...so are plain arrays...
232-
if (Array.isArray(value)) {
233-
value.forEach((child, i) => {
234-
check_serializability(child, id, `${path}[${i}]`);
235-
});
236-
return;
239+
throw new Error(
240+
`Data returned from \`action\` inside ${routeId} must be a plain object${
241+
constructorName ? ` rather than an instance of ${constructorName}` : ''
242+
}`
243+
);
237244
}
238245

239-
// ...and objects
240-
// This simple check might potentially run into some weird edge cases
241-
// Refer to https://github.com/lodash/lodash/blob/2da024c3b4f9947a48517639de7560457cd4ec6c/isPlainObject.js?rgh-link-date=2022-07-20T12%3A48%3A07Z#L30
242-
// if that ever happens
243-
if (Object.getPrototypeOf(value) === Object.prototype) {
244-
for (const key in value) {
245-
check_serializability(value[key], id, `${path}.${key}`);
246-
}
247-
return;
248-
}
246+
throw error;
249247
}
250-
251-
throw new Error(
252-
`${path} returned from action in ${id} cannot be serialized as JSON without losing its original type` +
253-
// probably the most common case, so let's give a hint
254-
(value instanceof Date ? ' (Date objects are serialized as strings)' : '')
255-
);
256248
}

packages/kit/src/runtime/server/page/render.js

+2-2
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@ import { hash } from '../../hash.js';
44
import { serialize_data } from './serialize_data.js';
55
import { s } from '../../../utils/misc.js';
66
import { Csp } from './csp.js';
7+
import { stringify_action_response } from './actions.js';
78

89
// TODO rename this function/module
910

@@ -200,8 +201,7 @@ export async function render_response({
200201
}
201202

202203
if (form_value) {
203-
// no need to check it can be serialized, we already verified that it's JSON-friendly
204-
serialized.form = devalue.uneval(form_value);
204+
serialized.form = stringify_action_response(form_value, event.routeId);
205205
}
206206

207207
if (inline_styles.size > 0) {

0 commit comments

Comments
 (0)