|
| 1 | +import * as devalue from 'devalue'; |
1 | 2 | import { error, json } from '../../../exports/index.js';
|
2 | 3 | import { normalize_error } from '../../../utils/error.js';
|
3 | 4 | import { is_form_content_type, negotiate } from '../../../utils/http.js';
|
@@ -41,14 +42,20 @@ export async function handle_action_json_request(event, options, server) {
|
41 | 42 | const data = await call_action(event, actions);
|
42 | 43 |
|
43 | 44 | if (data instanceof ValidationError) {
|
44 |
| - check_serializability(data.data, /** @type {string} */ (event.route.id), '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 | + // @ts-expect-error we assign a string to what is supposed to be an object. That's ok |
| 49 | + // because we don't use the object outside, and this way we have better code navigation |
| 50 | + // through knowing where the related interface is used. |
| 51 | + data: stringify_action_response(data.data, /** @type {string} */ (event.route.id)) |
| 52 | + }); |
46 | 53 | } else {
|
47 |
| - check_serializability(data, /** @type {string} */ (event.route.id), 'data'); |
48 | 54 | return action_json({
|
49 | 55 | type: 'success',
|
50 | 56 | status: data ? 200 : 204,
|
51 |
| - data: /** @type {Record<string, any> | undefined} */ (data) |
| 57 | + // @ts-expect-error see comment above |
| 58 | + data: stringify_action_response(data, /** @type {string} */ (event.route.id)) |
52 | 59 | });
|
53 | 60 | }
|
54 | 61 | } catch (e) {
|
@@ -211,46 +218,41 @@ function maybe_throw_migration_error(server) {
|
211 | 218 | }
|
212 | 219 |
|
213 | 220 | /**
|
214 |
| - * Check that the data can safely be serialized to JSON |
215 |
| - * @param {any} value |
216 |
| - * @param {string} id |
217 |
| - * @param {string} path |
| 221 | + * Try to `devalue.uneval` the data object, and if it fails, return a proper Error with context |
| 222 | + * @param {any} data |
| 223 | + * @param {string} route_id |
218 | 224 | */
|
219 |
| -function check_serializability(value, id, path) { |
220 |
| - const type = typeof value; |
| 225 | +export function uneval_action_response(data, route_id) { |
| 226 | + return try_deserialize(data, devalue.uneval, route_id); |
| 227 | +} |
221 | 228 |
|
222 |
| - if (type === 'string' || type === 'boolean' || type === 'number' || type === 'undefined') { |
223 |
| - // primitives are fine |
224 |
| - return; |
225 |
| - } |
| 229 | +/** |
| 230 | + * Try to `devalue.stringify` the data object, and if it fails, return a proper Error with context |
| 231 | + * @param {any} data |
| 232 | + * @param {string} route_id |
| 233 | + */ |
| 234 | +function stringify_action_response(data, route_id) { |
| 235 | + return try_deserialize(data, devalue.stringify, route_id); |
| 236 | +} |
226 | 237 |
|
227 |
| - if (type === 'object') { |
228 |
| - // nulls are fine... |
229 |
| - if (!value) return; |
| 238 | +/** |
| 239 | + * @param {any} data |
| 240 | + * @param {(data: any) => string} fn |
| 241 | + * @param {string} route_id |
| 242 | + */ |
| 243 | +function try_deserialize(data, fn, route_id) { |
| 244 | + try { |
| 245 | + return fn(data); |
| 246 | + } catch (e) { |
| 247 | + // If we're here, the data could not be serialized with devalue |
| 248 | + const error = /** @type {any} */ (e); |
230 | 249 |
|
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; |
| 250 | + if ('path' in error) { |
| 251 | + let message = `Data returned from action inside ${route_id} is not serializable: ${error.message}`; |
| 252 | + if (error.path !== '') message += ` (data.${error.path})`; |
| 253 | + throw new Error(message); |
237 | 254 | }
|
238 | 255 |
|
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 |
| - } |
| 256 | + throw error; |
249 | 257 | }
|
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 |
| - ); |
256 | 258 | }
|
0 commit comments