Skip to content

Fix: Handle circular references in Http.request JSON processing #10744

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Open
wants to merge 2 commits into
base: main
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
31 changes: 28 additions & 3 deletions packages/core/http/http-request/index.android.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
// imported for definition purposes only
import * as httpModule from '../../http';
import { safeJsonStringify } from '../../utils/safe-json-stringify';
import * as imageSourceModule from '../../image-source';
import { Screen } from '../../platform';
import * as fsModule from '../../file-system';
Expand Down Expand Up @@ -92,11 +93,15 @@ function onRequestComplete(requestId: number, result: org.nativescript.widgets.A

debugRequest.mimeType = mime;
debugRequest.data = result.raw;

// Create a safe version of headers for debugging
const safeDebugHeaders = JSON.parse(safeJsonStringify(headers));

const debugResponse = {
url: result.url,
status: result.statusCode,
statusText: result.statusText,
headers: headers,
headers: safeDebugHeaders, // Use the safe version
mimeType: mime,
fromDiskCache: false,
timing: {
Expand Down Expand Up @@ -272,12 +277,32 @@ export function request(options: httpModule.HttpRequestOptions): Promise<httpMod
debugRequest,
timestamp,
});
const originalRequestHeaders = options.headers;
const safeRequestHeaders = JSON.parse(safeJsonStringify(originalRequestHeaders || {})); // Handle undefined headers

const request = {
url: options.url,
method: 'GET',
headers: options.headers,
method: 'GET', // Default, will be overridden
headers: safeRequestHeaders, // Use the safe version
timestamp,
};
// If options.method exists, use it, otherwise default to 'GET' for the debug message
if (options.method) {
request.method = options.method;
}
// if options.content exists, add it to the debug request postData
if (options.content) {
if (typeof options.content === 'string') {
(request as any).postData = options.content;
} else if (options.content instanceof FormData) {
// FormData might be complex, stringify safely or send as string
(request as any).postData = safeJsonStringify(options.content);
} else if (options.content instanceof ArrayBuffer) {
// ArrayBuffer is not circular, but might be converted to string by debugger
// For now, let debugger handle it or send a placeholder
(request as any).postData = '[ArrayBuffer]';
}
}
debugRequest.requestWillBeSent(request);
}
}
Expand Down
115 changes: 115 additions & 0 deletions packages/core/utils/safe-json-stringify.spec.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,115 @@
import { safeJsonStringify } from './safe-json-stringify';

describe('safeJsonStringify', () => {
it('should stringify an object without circular references', () => {
const obj = { a: 1, b: 'test', c: { d: 2 } };
const json = safeJsonStringify(obj);
expect(json).toBe('{"a":1,"b":"test","c":{"d":2}}');
});

it('should handle direct circular references', () => {
const obj: any = { a: 1 };
obj.b = obj; // Circular reference
const json = safeJsonStringify(obj);
expect(json).toBe('{"a":1,"b":"[Circular]"}');
});

it('should handle nested circular references', () => {
const obj: any = { a: { b: { c: 1 } } };
obj.a.b.d = obj.a; // Circular reference
const json = safeJsonStringify(obj);
expect(json).toBe('{"a":{"b":{"c":1,"d":"[Circular]"}}}');
});

it('should handle circular references in an array', () => {
const arr: any[] = [1, 2];
arr.push(arr); // Circular reference
const obj = { data: arr };
const json = safeJsonStringify(obj);
expect(json).toBe('{"data":[1,2,"[Circular]"]}');
});

it('should handle multiple circular references to the same object', () => {
const circularA: any = { id: 'a' };
circularA.self = circularA;

const container = { item1: circularA, item2: circularA };
const json = safeJsonStringify(container);
// When container is stringified:
// item1 is processed. circularA is stringified as {"id":"a","self":"[Circular]"}.
// circularA instance is added to the cache.
// item2 is processed. Its value is circularA, which is already in the cache.
// So, item2 should be replaced with "[Circular]".
expect(json).toBe('{"item1":{"id":"a","self":"[Circular]"},"item2":"[Circular]"}');
});

it('should handle null and undefined values correctly in objects', () => {
const obj = { a: null, b: undefined, c: { d: null } };
const json = safeJsonStringify(obj);
// Note: JSON.stringify removes properties with undefined values from objects.
expect(json).toBe('{"a":null,"c":{"d":null}}');
});

it('should handle basic data types', () => {
expect(safeJsonStringify(123)).toBe('123');
expect(safeJsonStringify("hello")).toBe('"hello"');
expect(safeJsonStringify(true)).toBe('true');
expect(safeJsonStringify(null)).toBe('null');
// JSON.stringify(undefined) returns undefined (the value, not string)
// If safeJsonStringify is strictly typed to return string, it must handle this.
// Assuming it's modified to return "undefined" string literal for undefined input:
// This test will depend on the final implementation of safeJsonStringify for this edge case.
// For now, let's assume the current implementation where JSON.stringify might return undefined
// and the type signature is `string`. This implies a potential issue to be fixed in safeJsonStringify.
// However, test runners might stringify undefined to "undefined" in expect().
// Let's test against what JSON.stringify actually does with the replacer.
// The replacer will get (key="", value=undefined) and return undefined.
// JSON.stringify(undefined) is undefined.
// So, this test case needs clarification on desired behavior vs. actual.
// If the function MUST return a string, it needs modification.
// If it can return `undefined` (violating type hint), then `expect(...).toBe(undefined)` is correct.
// Given the type hint is `string`, we expect a string.
// Let's assume the function is corrected to return "undefined" string for undefined input.
// This will require a change in safe-json-stringify.ts
expect(safeJsonStringify(undefined)).toBe("undefined");
});

it('should handle arrays with basic types including undefined', () => {
const arr = [1, "two", true, null, undefined];
const json = safeJsonStringify(arr);
// Note: JSON.stringify converts undefined in arrays to null.
expect(json).toBe('[1,"two",true,null,null]');
});

it('should handle object with a toJSON method that causes circularity by returning itself', () => {
const obj: any = {
name: 'Initial',
toJSON: function() {
return this;
}
};
// Replacer sees obj (key=""). Adds to cache. Returns obj.
// Stringify calls toJSON on obj. It returns obj.
// Stringify sees the result (obj) needs serialization. Calls replacer(key="", obj).
// Obj is in cache. Replacer returns "[Circular]".
// Stringify then stringifies the string "[Circular]".
const json = safeJsonStringify(obj);
expect(json).toBe('"\"[Circular]\""'); // Stringified version of "[Circular]"
});

it('should handle an object whose toJSON method returns a new object with a circular reference', () => {
const child: any = { x: 10 };
child.parent = child; // child is circular

const obj = {
name: 'Container',
childHolder: {
toJSON: function() {
return child; // toJSON returns an object that is already circular
}
}
};
const json = safeJsonStringify(obj);
expect(json).toBe('{"name":"Container","childHolder":{"x":10,"parent":"[Circular]"}}');
});
});
15 changes: 15 additions & 0 deletions packages/core/utils/safe-json-stringify.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
export function safeJsonStringify(obj: any): string {
const cache = new Set();
const result = JSON.stringify(obj, (key, value) => {
if (typeof value === 'object' && value !== null) {
if (cache.has(value)) {
// Circular reference found, discard key
return '[Circular]';
}
// Store value in our collection
cache.add(value);
}
return value;
});
return result === undefined ? "undefined" : result;
}