Skip to content
15 changes: 15 additions & 0 deletions spec/common/providers/https.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -949,6 +949,21 @@ describe("encoding/decoding", () => {
});
});

it("Throws object with self reference", () => {
class TestClass {
foo: string;
bar: number;
self: TestClass;
constructor(foo: string, bar: number) {
this.foo = foo;
this.bar = bar;
this.self = this;
}
}
const testObject = new TestClass("hello", 1);
expect(()=>https.encode(testObject)).to.throw(`Data cannot be encoded in JSON: ${testObject}`);
});

it("encodes function as an empty object", () => {
expect(https.encode(() => "foo")).to.deep.equal({});
});
Expand Down
44 changes: 32 additions & 12 deletions src/common/providers/https.ts
Original file line number Diff line number Diff line change
Expand Up @@ -404,12 +404,14 @@ const LONG_TYPE = "type.googleapis.com/google.protobuf.Int64Value";
/** @hidden */
const UNSIGNED_LONG_TYPE = "type.googleapis.com/google.protobuf.UInt64Value";

/** @hidden */
const SELF_REFERENCE_WEAKSET = new WeakSet<object|((...args:any[])=>any)>();
/**
* Encodes arbitrary data in our special format for JSON.
* This is exposed only for testing.
*/
/** @hidden */
export function encode(data: any): any {
export function encode(data: unknown): any {
if (data === null || typeof data === "undefined") {
return null;
}
Expand All @@ -430,18 +432,36 @@ export function encode(data: any): any {
if (Array.isArray(data)) {
return data.map(encode);
}
if (typeof data === "object" || typeof data === "function") {
// Sadly we don't have Object.fromEntries in Node 10, so we can't use a single
// list comprehension
const obj: Record<string, any> = {};
for (const [k, v] of Object.entries(data)) {
obj[k] = encode(v);
}
return obj;
if(!isFunctionOrObject(data)||SELF_REFERENCE_WEAKSET.has(data)) {
logger.error("Data cannot be encoded in JSON.", data);
throw new Error(`Data cannot be encoded in JSON: ${data}`);
}

// Sadly we don't have Object.fromEntries in Node 10, so we can't use a single
// list comprehension
const obj: Record<string, any> = {};

SELF_REFERENCE_WEAKSET.add(data);

for (const [k, v] of Object.entries(data)) {
obj[k] = encode(v);
}
// If we got this far, the data is not encodable.
logger.error("Data cannot be encoded in JSON.", data);
throw new Error(`Data cannot be encoded in JSON: ${data}`);

// clean after recursive call -
// we don't want to keep references to objects that are not part of the current object
SELF_REFERENCE_WEAKSET.delete(data);

return obj;
}

function isFunctionOrObject(data: unknown): data is object|((...args:any[])=>any) {
const isObjectOrFunction = typeof data === "object" || typeof data === "function";

if (!isObjectOrFunction) {
// If we got this far, the data is not encodable.
return false;
}
return true;
}

/**
Expand Down