|
| 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,16 @@ 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.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 | + }); |
46 | 50 | } else {
|
47 |
| - check_serializability(data, /** @type {string} */ (event.routeId), 'data'); |
48 | 51 | return action_json({
|
49 | 52 | type: 'success',
|
50 | 53 | status: data ? 200 : 204,
|
51 |
| - data: /** @type {Record<string, any> | undefined} */ (data) |
| 54 | + data: uneval(data, /** @type {string} */ (event.routeId)) |
52 | 55 | });
|
53 | 56 | }
|
54 | 57 | } catch (e) {
|
@@ -211,46 +214,35 @@ function maybe_throw_migration_error(server) {
|
211 | 214 | }
|
212 | 215 |
|
213 | 216 | /**
|
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 |
218 | 220 | */
|
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 | + } |
221 | 233 |
|
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); |
226 | 235 |
|
227 |
| - if (type === 'object') { |
228 |
| - // nulls are fine... |
229 |
| - if (!value) return; |
| 236 | + if (nonPojoError) { |
| 237 | + const constructorName = data?.constructor?.name; |
230 | 238 |
|
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 | + ); |
237 | 244 | }
|
238 | 245 |
|
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; |
249 | 247 | }
|
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 | 248 | }
|
0 commit comments