|
| 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