Non è possibile crittografare un errore utilizzando JSON.stringify?


331

Riproduzione del problema

Sto riscontrando un problema quando provo a trasmettere messaggi di errore utilizzando i socket Web. Posso replicare il problema che sto affrontando utilizzando JSON.stringifyper soddisfare un pubblico più ampio:

// node v0.10.15
> var error = new Error('simple error message');
    undefined

> error
    [Error: simple error message]

> Object.getOwnPropertyNames(error);
    [ 'stack', 'arguments', 'type', 'message' ]

> JSON.stringify(error);
    '{}'

Il problema è che finisco con un oggetto vuoto.

Quello che ho provato

browser

Per prima cosa ho provato a lasciare node.js ed eseguirlo in vari browser. La versione 28 di Chrome mi dà lo stesso risultato e, cosa abbastanza interessante, almeno un tentativo di Firefox ha lasciato fuori il messaggio:

>>> JSON.stringify(error); // Firebug, Firefox 23
{"fileName":"debug eval code","lineNumber":1,"stack":"@debug eval code:1\n"}

Funzione di sostituzione

Ho quindi esaminato il Error.prototype . Mostra che il prototipo contiene metodi come toString e toSource . Sapendo che non è possibile restringere le funzioni, ho incluso una funzione di sostituzione quando ho chiamato JSON.stringify per rimuovere tutte le funzioni, ma poi ho capito che anche questo aveva un comportamento strano:

var error = new Error('simple error message');
JSON.stringify(error, function(key, value) {
    console.log(key === ''); // true (?)
    console.log(value === error); // true (?)
});

Non sembra passare sopra l'oggetto come farebbe normalmente, e quindi non posso controllare se il tasto è una funzione e ignorarlo.

La domanda

Esiste un modo per stringere i messaggi di errore nativi con JSON.stringify? In caso contrario, perché si verifica questo comportamento?

Metodi per aggirare questo

  • Attenersi a semplici messaggi di errore basati su stringhe o creare oggetti di errore personali e non fare affidamento sull'oggetto Error nativo.
  • Proprietà pull: JSON.stringify({ message: error.message, stack: error.stack })

aggiornamenti

@Ray Toal Suggerito in un commento che do un'occhiata ai descrittori di proprietà . Ora è chiaro perché non funziona:

var error = new Error('simple error message');
var propertyNames = Object.getOwnPropertyNames(error);
var descriptor;
for (var property, i = 0, len = propertyNames.length; i < len; ++i) {
    property = propertyNames[i];
    descriptor = Object.getOwnPropertyDescriptor(error, property);
    console.log(property, descriptor);
}

Produzione:

stack { get: [Function],
  set: [Function],
  enumerable: false,
  configurable: true }
arguments { value: undefined,
  writable: true,
  enumerable: false,
  configurable: true }
type { value: undefined,
  writable: true,
  enumerable: false,
  configurable: true }
message { value: 'simple error message',
  writable: true,
  enumerable: false,
  configurable: true }

Legenda: enumerable: false.

La risposta accettata fornisce una soluzione alternativa per questo problema.


3
Hai esaminato i descrittori di proprietà per le proprietà nell'oggetto errore?
Ray Toal,

3
La domanda per me era "perché" e ho scoperto che la risposta era in fondo alla domanda. Non c'è niente di sbagliato nel pubblicare una risposta per la tua domanda e probabilmente otterrai più credito in questo modo. :-)
Michael Scheper,

Risposte:


178

È possibile definire a Error.prototype.toJSONper recuperare una pianura che Objectrappresenta Error:

if (!('toJSON' in Error.prototype))
Object.defineProperty(Error.prototype, 'toJSON', {
    value: function () {
        var alt = {};

        Object.getOwnPropertyNames(this).forEach(function (key) {
            alt[key] = this[key];
        }, this);

        return alt;
    },
    configurable: true,
    writable: true
});
var error = new Error('testing');
error.detail = 'foo bar';

console.log(JSON.stringify(error));
// {"message":"testing","detail":"foo bar"}

L'utilizzo Object.defineProperty()aggiunge toJSONsenza che sia una enumerableproprietà stessa.


Per quanto riguarda la modifica Error.prototype, anche se toJSON()potrebbe non essere definito per Errors in modo specifico, il metodo è ancora standardizzato per gli oggetti in generale (rif: passaggio 3). Pertanto, il rischio di collisioni o conflitti è minimo.

Tuttavia, per evitarlo ancora completamente, JSON.stringify()èreplacer possibile utilizzare invece il parametro :

function replaceErrors(key, value) {
    if (value instanceof Error) {
        var error = {};

        Object.getOwnPropertyNames(value).forEach(function (key) {
            error[key] = value[key];
        });

        return error;
    }

    return value;
}

var error = new Error('testing');
error.detail = 'foo bar';

console.log(JSON.stringify(error, replaceErrors));

3
Se usi .getOwnPropertyNames()invece di .keys(), otterrai le proprietà non enumerabili senza doverle definire manualmente.

8
Meglio non aggiungerlo a Error.prototype, può dare problemi quando in una versione futura di JavaScrip Error.prototype ha effettivamente una funzione toJSON.
Jos de Jong,

3
! attento Questa soluzione interrompe la gestione degli errori nel driver mongodb del
Sebastian Nowak,

5
Nel caso in cui qualcuno presta attenzione agli errori del linker e ai conflitti di denominazione: se si utilizza l'opzione di sostituzione, è necessario scegliere un nome di parametro diverso per keyin function replaceErrors(key, value)per evitare conflitti di denominazione con .forEach(function (key) { .. }); il replaceErrors keyparametro non è utilizzato in questa risposta.
404 non trovato il

2
L'ombra di keyquesto esempio, sebbene consentita, è potenzialmente fonte di confusione in quanto lascia dubbi sul fatto che l'autore abbia intenzione di fare riferimento alla variabile esterna o meno. propNamesarebbe una scelta più espressiva per il ciclo interno. (A proposito, penso che @ 404NotFound significhi " linter " (strumento di analisi statica) non "linker" ) In ogni caso, l'utilizzo di una replacerfunzione personalizzata è un'ottima soluzione per questo in quanto risolve il problema in un posto appropriato e non altera il nativo / comportamento globale.
iX3,

261
JSON.stringify(err, Object.getOwnPropertyNames(err))

sembra funzionare

[ da un commento di / u / ub3rgeek su / r / javascript ] e il commento di felixfbecker qui sotto


57
Pettinando le risposte,JSON.stringify(err, Object.getOwnPropertyNames(err))
felixfbecker,

5
Funziona bene con un oggetto Error ExpressJS nativo, ma non funziona con un errore Mongoose. Gli errori Mongoose hanno oggetti nidificati per ValidationErrortipi. Ciò non stringerà l' errorsoggetto nidificato in un oggetto errore Mongoose di tipo ValidationError.
potenziato a vapore il

4
questa dovrebbe essere la risposta, perché è il modo più semplice per farlo.
Huan,

7
@felixfbecker Cerca solo i nomi di proprietà a un livello di profondità . Se hai var spam = { a: 1, b: { b: 2, b2: 3} };ed esegui Object.getOwnPropertyNames(spam), otterrai ["a", "b"]- ingannevole qui, perché l' boggetto ha il suo b. Otterresti entrambi nella tua chiamata stringify, ma ti mancherebbespam.b.b2 . Questo è male.
ruffin,

1
@ruffin è vero, ma potrebbe anche essere desiderabile. Penso che OP volesse solo accertarsene messagee stacksono inclusi in JSON.
felixfbecker,

74

Poiché nessuno sta parlando della parte del perché , risponderò.

Perché questo JSON.stringifyrestituisce un oggetto vuoto?

> JSON.stringify(error);
'{}'

Risposta

Dal documento di JSON.stringify () ,

Per tutte le altre istanze di oggetti (inclusi Map, Set, WeakMap e WeakSet), verranno serializzate solo le loro proprietà enumerabili.

e l' Erroroggetto non ha le sue proprietà enumerabili, ecco perché stampa un oggetto vuoto.


4
Strano che nessuno si sia nemmeno preoccupato. Finché funziona la correzione presumo :)
Ilya Chernomordik il

1
La prima parte di questa risposta non è corretta. C'è un modo per usare JSON.stringifyusando il suo replacerparametro.
Todd Chaffee,

1
@ToddChaffee è un buon punto. Ho corretto la mia risposta. Controlla e sentiti libero di migliorarlo. Grazie.
Sanghyun Lee,

52

Modifica dell'ottima risposta di Jonathan per evitare di rattoppare le scimmie:

var stringifyError = function(err, filter, space) {
  var plainObject = {};
  Object.getOwnPropertyNames(err).forEach(function(key) {
    plainObject[key] = err[key];
  });
  return JSON.stringify(plainObject, filter, space);
};

var error = new Error('testing');
error.detail = 'foo bar';

console.log(stringifyError(error, null, '\t'));

3
La prima volta che ho sentito monkey patching:)
Chris Prince, il

2
@ChrisPrince Ma non sarà l'ultima volta, specialmente in JavaScript! Ecco Wikipedia su Monkey Patching , solo per informazioni sulla gente futura. (In risposta di Jonathan , come capisce Chris, si sta aggiungendo una nuova funzione, toJSON, direttamente a Error's prototipo , che spesso non è una grande idea. Forse qualcun altro ha già, che quest'anno i controlli, ma poi non so cosa quell'altra versione. O se qualcuno prende inaspettatamente il tuo, o presume che il prototipo di Error abbia proprietà specifiche, le cose potrebbero andare male.)
Ruffin

questo è carino, ma omette lo stack dell'errore (che viene mostrato nella console). non sono sicuro dei dettagli, se questo è legato a Vue o cosa, volevo solo menzionarlo.
phil294

23

V'è un grande pacchetto Node.js per questo: serialize-error.

Gestisce bene anche gli oggetti Error nidificati, ciò di cui in realtà avevo molto bisogno nel mio progetto.

https://www.npmjs.com/package/serialize-error


No, ma può essere traspilato per farlo. Vedere questo commento .
iX3,

Questa è la risposta corretta La serializzazione degli errori non è un problema banale e l'autore della libreria (un eccellente sviluppatore con molti pacchetti molto popolari) ha fatto di tutto per gestire i casi limite, come si può vedere nel README: "Le proprietà personalizzate sono conservate. Non elencabile le proprietà vengono mantenute non enumerabili (nome, messaggio, stack). Le proprietà enumerabili vengono mantenute enumerabili (tutte le proprietà oltre a quelle non enumerabili). I riferimenti circolari vengono gestiti. "
Dan Dascalescu

9

È inoltre possibile ridefinire le proprietà non enumerabili per essere enumerabili.

Object.defineProperty(Error.prototype, 'message', {
    configurable: true,
    enumerable: true
});

e forse stackanche proprietà.


9
Non cambiare oggetti che non possiedi, può rompere altre parti della tua applicazione e buona fortuna scoprendo il perché.
fregante

7

Avevamo bisogno di serializzare una gerarchia di oggetti arbitraria, in cui la radice o una delle proprietà nidificate nella gerarchia potrebbero essere istanze di errore.

La nostra soluzione era usare il replacerparametro di JSON.stringify(), ad esempio:

function jsonFriendlyErrorReplacer(key, value) {
  if (value instanceof Error) {
    return {
      // Pull all enumerable properties, supporting properties on custom Errors
      ...value,
      // Explicitly pull Error's non-enumerable properties
      name: value.name,
      message: value.message,
      stack: value.stack,
    }
  }

  return value
}

let obj = {
    error: new Error('nested error message')
}

console.log('Result WITHOUT custom replacer:', JSON.stringify(obj))
console.log('Result WITH custom replacer:', JSON.stringify(obj, jsonFriendlyErrorReplacer))


5

Nessuna delle risposte sopra sembrava serializzare correttamente le proprietà che si trovano sul prototipo di Error (perché getOwnPropertyNames()non include le proprietà ereditate). Inoltre non sono stato in grado di ridefinire le proprietà come una delle risposte suggerite.

Questa è la soluzione che mi è venuta in mente: utilizza lodash ma è possibile sostituire lodash con versioni generiche di tali funzioni.

 function recursivePropertyFinder(obj){
    if( obj === Object.prototype){
        return {};
    }else{
        return _.reduce(Object.getOwnPropertyNames(obj), 
            function copy(result, value, key) {
                if( !_.isFunction(obj[value])){
                    if( _.isObject(obj[value])){
                        result[value] = recursivePropertyFinder(obj[value]);
                    }else{
                        result[value] = obj[value];
                    }
                }
                return result;
            }, recursivePropertyFinder(Object.getPrototypeOf(obj)));
    }
}


Error.prototype.toJSON = function(){
    return recursivePropertyFinder(this);
}

Ecco il test che ho fatto in Chrome:

var myError = Error('hello');
myError.causedBy = Error('error2');
myError.causedBy.causedBy = Error('error3');
myError.causedBy.causedBy.displayed = true;
JSON.stringify(myError);

{"name":"Error","message":"hello","stack":"Error: hello\n    at <anonymous>:66:15","causedBy":{"name":"Error","message":"error2","stack":"Error: error2\n    at <anonymous>:67:20","causedBy":{"name":"Error","message":"error3","stack":"Error: error3\n    at <anonymous>:68:29","displayed":true}}}  

2

Stavo lavorando su un formato JSON per gli appendici di log e sono finito qui cercando di risolvere un problema simile. Dopo un po ', mi sono reso conto che avrei potuto semplicemente fare in modo che Node facesse il lavoro:

const util = require("util");
...
return JSON.stringify(obj, (name, value) => {
    if (value instanceof Error) {
        return util.format(value);
    } else {
        return value;
    }
}

1
Dovrebbe essere instanceofe non instanceOf.
lakshman.pasala il
Utilizzando il nostro sito, riconosci di aver letto e compreso le nostre Informativa sui cookie e Informativa sulla privacy.
Licensed under cc by-sa 3.0 with attribution required.