Error de solicitud de envío de Chrome: TypeError: convertir estructura circular a JSON

384

Tengo lo siguiente ...

chrome.extension.sendRequest({
  req: "getDocument",
  docu: pagedoc,
  name: 'name'
}, function(response){
  var efjs = response.reply;
});

que llama a lo siguiente ...

case "getBrowserForDocumentAttribute":
  alert("ZOMG HERE");
  sendResponse({
    reply: getBrowserForDocumentAttribute(request.docu,request.name)
  });
  break;

Sin embargo, mi código nunca llega a "ZOMG AQUÍ", sino que arroja el siguiente error mientras se ejecuta chrome.extension.sendRequest

 Uncaught TypeError: Converting circular structure to JSON
 chromeHidden.JSON.stringify
 chrome.Port.postMessage
 chrome.initExtension.chrome.extension.sendRequest
 suggestQuery

¿Alguien tiene alguna idea de lo que está causando esto?

Skizit
fuente
2
Está intentando enviar un objeto que tiene referencias circulares. ¿Qué es pagedoc?
Felix Kling
99
¿Qué quiero decir con qué? 1. ¿Cuál es el valor de pagedoc? 2. Referencia circular:a = {}; a.b = a;
Felix Kling,
1
Ahh ... eso lo arregló! Si desea poner eso en una respuesta, ¡le daré crédito por ello!
Skizit
55
intente usar node.js: util.inspect
boldnik

Respuestas:

489

Significa que el objeto que pasa en la solicitud (supongo que es pagedoc) tiene una referencia circular, algo así como:

var a = {};
a.b = a;

JSON.stringify no puede convertir estructuras como esta.

NB : este sería el caso con los nodos DOM, que tienen referencias circulares, incluso si no están conectados al árbol DOM. Cada nodo tiene un ownerDocumentque se refiere documenten la mayoría de los casos. documenttiene una referencia al árbol DOM al menos a través document.bodyy document.body.ownerDocumentvuelve a referirse a él document, que es solo una de las múltiples referencias circulares en el árbol DOM.

Felix Kling
fuente
2
¡Gracias! Esto explica el problema que tengo. Pero, ¿cómo la referencia circular presente en los objetos DOM no causa ningún problema? ¿JSON stringificaría un documentobjeto?
pregunta el
3
@asgs: Se hace causa problemas, al menos en Chrome. Firefox parece ser un poco más inteligente al respecto, pero no sé exactamente qué está haciendo.
Felix Kling
¿Es posible "atrapar" este error y manejarlo?
Doug Molineux
2
@DougMolineux: Claro, puedes usarlo try...catchpara detectar este error.
Felix Kling
44
@FelixKling Desafortunadamente no pude hacer que eso funcionara (podría haber estado haciendo algo mal) Terminé usando esto: github.com/isaacs/json-stringify-safe
Doug Molineux el
128

Según los documentos JSON en Mozilla , JSON.Stringifytiene un segundo parámetro censorque se puede usar para filtrar / ignorar elementos secundarios mientras se analiza el árbol. Sin embargo, quizás pueda evitar las referencias circulares.

En Node.js no podemos. Entonces podemos hacer algo como esto:

function censor(censor) {
  var i = 0;

  return function(key, value) {
    if(i !== 0 && typeof(censor) === 'object' && typeof(value) == 'object' && censor == value) 
      return '[Circular]'; 

    if(i >= 29) // seems to be a harded maximum of 30 serialized objects?
      return '[Unknown]';

    ++i; // so we know we aren't using the original object anymore

    return value;  
  }
}

var b = {foo: {bar: null}};

b.foo.bar = b;

console.log("Censoring: ", b);

console.log("Result: ", JSON.stringify(b, censor(b)));

El resultado:

Censoring:  { foo: { bar: [Circular] } }
Result: {"foo":{"bar":"[Circular]"}}

Desafortunadamente, parece haber un máximo de 30 iteraciones antes de asumir automáticamente que es circular. De lo contrario, esto debería funcionar. Incluso lo usé areEquivalent desde aquí , pero JSON.Stringifyaún arroja la excepción después de 30 iteraciones. Aún así, es lo suficientemente bueno para obtener una representación decente del objeto en un nivel superior, si realmente lo necesita. ¿Quizás alguien pueda mejorar esto? En Node.js para un objeto de solicitud HTTP, obtengo:

{
"limit": null,
"size": 0,
"chunks": [],
"writable": true,
"readable": false,
"_events": {
    "pipe": [null, null],
    "error": [null]
},
"before": [null],
"after": [],
"response": {
    "output": [],
    "outputEncodings": [],
    "writable": true,
    "_last": false,
    "chunkedEncoding": false,
    "shouldKeepAlive": true,
    "useChunkedEncodingByDefault": true,
    "_hasBody": true,
    "_trailer": "",
    "finished": false,
    "socket": {
        "_handle": {
            "writeQueueSize": 0,
            "socket": "[Unknown]",
            "onread": "[Unknown]"
        },
        "_pendingWriteReqs": "[Unknown]",
        "_flags": "[Unknown]",
        "_connectQueueSize": "[Unknown]",
        "destroyed": "[Unknown]",
        "bytesRead": "[Unknown]",
        "bytesWritten": "[Unknown]",
        "allowHalfOpen": "[Unknown]",
        "writable": "[Unknown]",
        "readable": "[Unknown]",
        "server": "[Unknown]",
        "ondrain": "[Unknown]",
        "_idleTimeout": "[Unknown]",
        "_idleNext": "[Unknown]",
        "_idlePrev": "[Unknown]",
        "_idleStart": "[Unknown]",
        "_events": "[Unknown]",
        "ondata": "[Unknown]",
        "onend": "[Unknown]",
        "_httpMessage": "[Unknown]"
    },
    "connection": "[Unknown]",
    "_events": "[Unknown]",
    "_headers": "[Unknown]",
    "_headerNames": "[Unknown]",
    "_pipeCount": "[Unknown]"
},
"headers": "[Unknown]",
"target": "[Unknown]",
"_pipeCount": "[Unknown]",
"method": "[Unknown]",
"url": "[Unknown]",
"query": "[Unknown]",
"ended": "[Unknown]"
}

Creé un pequeño módulo Node.js para hacer esto aquí: https://github.com/ericmuyser/stringy ¡ Siéntase libre de mejorar / contribuir!

Eric Muyser
fuente
10
Es la primera vez que veo que se pasa una función que devuelve una función de ejecución automática que devuelve una función normal. Creo que entiendo por qué se hizo esto, pero no creo que hubiera encontrado esa solución yo mismo, y creo que podría recordar esta técnica mejor si pudiera ver otros ejemplos donde se necesita esta configuración . Dicho esto, ¿podría señalar alguna literatura sobre esta configuración / técnica (por falta de una mejor palabra) o similares?
Shawn
1
+1 a Shawn. Elimine ese IEFE, es absolutamente inútil e ilegible.
Bergi
1
Gracias por señalar el censor arg! Permite depurar problemas circulares. en mi caso tuve una matriz jquery donde pensé tener una matriz normal. Ambos se ven similares en el modo de impresión de depuración. Sobre el IEFE, veo que se usan con frecuencia en lugares donde no hay absolutamente ninguna necesidad de ellos y estoy de acuerdo con Shawn y Bergi en que es así.
citykid
1
No estoy seguro de por qué, pero esta solución no parece funcionar para mí.
Nikola Schou el
1
@BrunoLM: para un límite de 30 iteraciones, si regresa '[Unknown:' + typeof(value) + ']', verá cómo arreglar el censor para tratar adecuadamente las funciones y algunos otros tipos.
Alex Pakka
46

Un enfoque es quitar objetos y funciones del objeto principal. Y stringify la forma más simple

function simpleStringify (object){
    var simpleObject = {};
    for (var prop in object ){
        if (!object.hasOwnProperty(prop)){
            continue;
        }
        if (typeof(object[prop]) == 'object'){
            continue;
        }
        if (typeof(object[prop]) == 'function'){
            continue;
        }
        simpleObject[prop] = object[prop];
    }
    return JSON.stringify(simpleObject); // returns cleaned up JSON
};
zainengineer
fuente
2
La respuesta perfecta para mi. ¿Tal vez la palabra clave 'función' se perdió?
Stepan Loginov
28

Normalmente uso el paquete circular-json npm para resolver esto.

// Felix Kling's example
var a = {};
a.b = a;
// load circular-json module
var CircularJSON = require('circular-json');
console.log(CircularJSON.stringify(a));
//result
{"b":"~"}

Nota: circular-json ha quedado en desuso, ahora uso flatted (del creador de CircularJSON):

// ESM
import {parse, stringify} from 'flatted/esm';

// CJS
const {parse, stringify} = require('flatted/cjs');

const a = [{}];
a[0].a = a;
a.push(a);

stringify(a); // [["1","0"],{"a":"0"}]

de: https://www.npmjs.com/package/flatted

usuario3139574
fuente
8

Basado en la respuesta de zainengineer ... Otro enfoque es hacer una copia profunda del objeto y quitar referencias circulares y stringificar el resultado.

function cleanStringify(object) {
    if (object && typeof object === 'object') {
        object = copyWithoutCircularReferences([object], object);
    }
    return JSON.stringify(object);

    function copyWithoutCircularReferences(references, object) {
        var cleanObject = {};
        Object.keys(object).forEach(function(key) {
            var value = object[key];
            if (value && typeof value === 'object') {
                if (references.indexOf(value) < 0) {
                    references.push(value);
                    cleanObject[key] = copyWithoutCircularReferences(references, value);
                    references.pop();
                } else {
                    cleanObject[key] = '###_Circular_###';
                }
            } else if (typeof value !== 'function') {
                cleanObject[key] = value;
            }
        });
        return cleanObject;
    }
}

// Example

var a = {
    name: "a"
};

var b = {
    name: "b"
};

b.a = a;
a.b = b;

console.log(cleanStringify(a));
console.log(cleanStringify(b));

CM
fuente
4

Resuelvo este problema en NodeJS así:

var util = require('util');

// Our circular object
var obj = {foo: {bar: null}, a:{a:{a:{a:{a:{a:{a:{hi: 'Yo!'}}}}}}}};
obj.foo.bar = obj;

// Generate almost valid JS object definition code (typeof string)
var str = util.inspect(b, {depth: null});

// Fix code to the valid state (in this example it is not required, but my object was huge and complex, and I needed this for my case)
str = str
    .replace(/<Buffer[ \w\.]+>/ig, '"buffer"')
    .replace(/\[Function]/ig, 'function(){}')
    .replace(/\[Circular]/ig, '"Circular"')
    .replace(/\{ \[Function: ([\w]+)]/ig, '{ $1: function $1 () {},')
    .replace(/\[Function: ([\w]+)]/ig, 'function $1(){}')
    .replace(/(\w+): ([\w :]+GMT\+[\w \(\)]+),/ig, '$1: new Date("$2"),')
    .replace(/(\S+): ,/ig, '$1: null,');

// Create function to eval stringifyed code
var foo = new Function('return ' + str + ';');

// And have fun
console.log(JSON.stringify(foo(), null, 4));
MiF
fuente
2

Experimenté el mismo error al intentar construir el mensaje a continuación con jQuery. La referencia circular ocurre cuando reviewerNamese le asignó por error msg.detail.reviewerName. El .val () de JQuery solucionó el problema, vea la última línea.

var reviewerName = $('reviewerName'); // <input type="text" id="taskName" />;
var msg = {"type":"A", "detail":{"managerReview":true} };
msg.detail.reviewerName = reviewerName; // Error
msg.detail.reviewerName = reviewerName.val(); // Fixed
izilotti
fuente
1

Estaba recibiendo el mismo error con jQuery formvaliadator, pero cuando eliminé un console.log dentro del éxito: funcionó, funcionó.

Azmeer
fuente
0

Para mi caso, recibí ese error cuando estaba usando la asyncfunción en mi lado del servidor para buscar documentos usando mangosta. Resultó que la razón fue que olvidé ponerlo awaitantes de llamar al find({})método. Agregar esa parte solucionó mi problema.

Mussa Charles
fuente
0

Esto funciona y le dice qué propiedades son circulares. También permite reconstruir el objeto con las referencias.

  JSON.stringifyWithCircularRefs = (function() {
    const refs = new Map();
    const parents = [];
    const path = ["this"];

    function clear() {
      refs.clear();
      parents.length = 0;
      path.length = 1;
    }

    function updateParents(key, value) {
      var idx = parents.length - 1;
      var prev = parents[idx];
      if (prev[key] === value || idx === 0) {
        path.push(key);
        parents.push(value);
      } else {
        while (idx-- >= 0) {
          prev = parents[idx];
          if (prev[key] === value) {
            idx += 2;
            parents.length = idx;
            path.length = idx;
            --idx;
            parents[idx] = value;
            path[idx] = key;
            break;
          }
        }
      }
    }

    function checkCircular(key, value) {
      if (value != null) {
        if (typeof value === "object") {
          if (key) { updateParents(key, value); }

          let other = refs.get(value);
          if (other) {
            return '[Circular Reference]' + other;
          } else {
            refs.set(value, path.join('.'));
          }
        }
      }
      return value;
    }

    return function stringifyWithCircularRefs(obj, space) {
      try {
        parents.push(obj);
        return JSON.stringify(obj, checkCircular, space);
      } finally {
        clear();
      }
    }
  })();

Ejemplo con mucho ruido eliminado:

{
    "requestStartTime": "2020-05-22...",
    "ws": {
        "_events": {},
        "readyState": 2,
        "_closeTimer": {
            "_idleTimeout": 30000,
            "_idlePrev": {
                "_idleNext": "[Circular Reference]this.ws._closeTimer",
                "_idlePrev": "[Circular Reference]this.ws._closeTimer",
                "expiry": 33764,
                "id": -9007199254740987,
                "msecs": 30000,
                "priorityQueuePosition": 2
            },
            "_idleNext": "[Circular Reference]this.ws._closeTimer._idlePrev",
            "_idleStart": 3764,
            "_destroyed": false
        },
        "_closeCode": 1006,
        "_extensions": {},
        "_receiver": {
            "_binaryType": "nodebuffer",
            "_extensions": "[Circular Reference]this.ws._extensions",
        },
        "_sender": {
            "_extensions": "[Circular Reference]this.ws._extensions",
            "_socket": {
                "_tlsOptions": {
                    "pipe": false,
                    "secureContext": {
                        "context": {},
                        "singleUse": true
                    },
                },
                "ssl": {
                    "_parent": {
                        "reading": true
                    },
                    "_secureContext": "[Circular Reference]this.ws._sender._socket._tlsOptions.secureContext",
                    "reading": true
                }
            },
            "_firstFragment": true,
            "_compress": false,
            "_bufferedBytes": 0,
            "_deflating": false,
            "_queue": []
        },
        "_socket": "[Circular Reference]this.ws._sender._socket"
    }
}

Para reconstruir, llame a JSON.parse () y luego recorra las propiedades buscando la [Circular Reference]etiqueta. Luego córtalo y ... evalúalo con el thisconjunto al objeto raíz.

No evalúes nada que pueda ser pirateado. Una mejor práctica sería hacer string.split('.')luego buscar las propiedades por nombre para establecer la referencia.

Derek Ziemba
fuente