Skip to content

Commit 59b2fad

Browse files
authored
Merge pull request #236 from Distributive-Network/Xmader/fix/socketio-connection-timeout
Fix Socket.IO connection timeout
2 parents aa5f991 + 4ac7aab commit 59b2fad

File tree

4 files changed

+58
-16
lines changed

4 files changed

+58
-16
lines changed

python/pythonmonkey/builtin_modules/XMLHttpRequest.js

Lines changed: 42 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -176,13 +176,27 @@ class XMLHttpRequest extends XMLHttpRequestEventTarget
176176
*/
177177
timeout = 0;
178178

179+
/**
180+
* A boolean value that indicates whether or not cross-site `Access-Control` requests should be made using credentials such as cookies, authorization headers or TLS client certificates.
181+
* Setting withCredentials has no effect on same-origin requests.
182+
* @see https://xhr.spec.whatwg.org/#the-withcredentials-attribute
183+
*/
179184
get withCredentials()
180185
{
181-
return false;
186+
return this.#crossOriginCredentials;
182187
}
183188
set withCredentials(flag)
184189
{
185-
throw new DOMException('xhr.withCredentials is not supported in PythonMonkey.', 'InvalidAccessError');
190+
// step 1
191+
if (this.#state !== XMLHttpRequest.UNSENT && this.#state !== XMLHttpRequest.OPENED)
192+
// The XHR internal state should be UNSENT or OPENED.
193+
throw new DOMException('XMLHttpRequest must not be sending.', 'InvalidStateError');
194+
// step 2
195+
if (this.#sendFlag)
196+
throw new DOMException('send() has already been called', 'InvalidStateError');
197+
// step 3
198+
this.#crossOriginCredentials = flag;
199+
// TODO: figure out what cross-origin means in PythonMonkey. Is it always same-origin request? What to send?
186200
}
187201

188202
/**
@@ -501,8 +515,8 @@ class XMLHttpRequest extends XMLHttpRequestEventTarget
501515
{
502516
if (this.#state === XMLHttpRequest.LOADING || this.#state === XMLHttpRequest.DONE)
503517
throw new DOMException('responseType can only be set before send()', 'InvalidStateError');
504-
if (!['', 'text', 'arraybuffer'].includes(t))
505-
throw new DOMException('only responseType "text" or "arraybuffer" is supported', 'NotSupportedError');
518+
if (!['', 'text', 'arraybuffer', 'json'].includes(t))
519+
throw new DOMException('only responseType "text" or "arraybuffer" or "json" is supported', 'NotSupportedError');
506520
this.#responseType = t;
507521
}
508522

@@ -536,7 +550,29 @@ class XMLHttpRequest extends XMLHttpRequestEventTarget
536550
this.#responseObject = this.#mergeReceivedBytes().buffer;
537551
return this.#responseObject;
538552
}
553+
554+
if (this.#responseType === 'json') // step 8
555+
{
556+
// step 8.2
557+
if (this.#receivedLength === 0) // response’s body is null
558+
return null;
559+
// step 8.3
560+
let jsonObject = null;
561+
try
562+
{
563+
// TODO: use proper TextDecoder API
564+
const str = decodeStr(this.#mergeReceivedBytes(), 'utf-8'); // only supports utf-8, see https://infra.spec.whatwg.org/#parse-json-bytes-to-a-javascript-value
565+
jsonObject = JSON.parse(str);
566+
}
567+
catch (exception)
568+
{
569+
return null;
570+
}
571+
// step 8.4
572+
this.#responseObject = jsonObject;
573+
}
539574

575+
// step 6 and step 7 ("blob" or "document") are not supported
540576
throw new DOMException(`unsupported responseType "${this.#responseType}"`, 'InvalidStateError');
541577
}
542578

@@ -565,6 +601,7 @@ class XMLHttpRequest extends XMLHttpRequestEventTarget
565601
#uploadObject = new XMLHttpRequestUpload();
566602
#state = XMLHttpRequest.UNSENT; // One of unsent, opened, headers received, loading, and done; initially unsent.
567603
#sendFlag = false; // A flag, initially unset.
604+
#crossOriginCredentials = false; // A boolean, initially false.
568605
/** @type {Method} */
569606
#requestMethod = null;
570607
/** @type {URL} */
@@ -585,7 +622,7 @@ class XMLHttpRequest extends XMLHttpRequestEventTarget
585622
#responseType = '';
586623
/**
587624
* cache for converting receivedBytes to the desired response type
588-
* @type {ArrayBuffer | string}
625+
* @type {ArrayBuffer | string | Record<any, any>}
589626
*/
590627
#responseObject = null;
591628

python/pythonmonkey/builtin_modules/timers.js

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -53,6 +53,10 @@ function setTimeout(handler, delayMs = 0, ...args)
5353
*/
5454
function clearTimeout(timeoutId)
5555
{
56+
// silently does nothing when an invalid timeoutId (should be an int32 value) is passed in
57+
if (!Number.isInteger(timeoutId))
58+
return;
59+
5660
return cancelByTimeoutId(timeoutId);
5761
}
5862

src/internalBinding/timers.cc

Lines changed: 2 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -32,25 +32,18 @@ static bool enqueueWithDelay(JSContext *cx, unsigned argc, JS::Value *vp) {
3232
PyEventLoop::AsyncHandle handle = loop.enqueueWithDelay(job, delaySeconds);
3333

3434
// Return the `timeoutID` to use in `clearTimeout`
35-
args.rval().setDouble((double)PyEventLoop::AsyncHandle::getUniqueId(std::move(handle)));
35+
args.rval().setNumber(PyEventLoop::AsyncHandle::getUniqueId(std::move(handle)));
3636
return true;
3737
}
3838

39-
// TODO (Tom Tang): move argument checks to the JavaScript side
4039
static bool cancelByTimeoutId(JSContext *cx, unsigned argc, JS::Value *vp) {
4140
using AsyncHandle = PyEventLoop::AsyncHandle;
4241
JS::CallArgs args = JS::CallArgsFromVp(argc, vp);
43-
JS::HandleValue timeoutIdArg = args.get(0);
42+
double timeoutID = args.get(0).toNumber();
4443

4544
args.rval().setUndefined();
4645

47-
// silently does nothing when an invalid timeoutID is passed in
48-
if (!timeoutIdArg.isInt32()) {
49-
return true;
50-
}
51-
5246
// Retrieve the AsyncHandle by `timeoutID`
53-
int32_t timeoutID = timeoutIdArg.toInt32();
5447
AsyncHandle *handle = AsyncHandle::fromId((uint32_t)timeoutID);
5548
if (!handle) return true; // does nothing on invalid timeoutID
5649

tests/python/test_event_loop.py

Lines changed: 10 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -37,8 +37,16 @@ def to_raise(msg):
3737
# `setTimeout` should allow passing additional arguments to the callback, as spec-ed
3838
assert 3.0 == await pm.eval("new Promise((resolve) => setTimeout(function(){ resolve(arguments.length) }, 100, 90, 91, 92))")
3939
assert 92.0 == await pm.eval("new Promise((resolve) => setTimeout((...args) => { resolve(args[2]) }, 100, 90, 91, 92))")
40-
# TODO (Tom Tang): test `setTimeout` setting delay to 0 if < 0
41-
# TODO (Tom Tang): test `setTimeout` accepting string as the delay, coercing to a number like parseFloat
40+
# test `setTimeout` setting delay to 0 if < 0
41+
await asyncio.wait_for(pm.eval("new Promise((resolve) => setTimeout(resolve, 0))"), timeout=0.05)
42+
await asyncio.wait_for(pm.eval("new Promise((resolve) => setTimeout(resolve, -10000))"), timeout=0.05) # won't be precisely 0s
43+
# test `setTimeout` accepting string as the delay, coercing to a number.
44+
# Number('100') -> 100, pass if the actual delay is > 90ms and < 150ms
45+
await asyncio.wait_for(pm.eval("new Promise((resolve) => setTimeout(resolve, '100'))"), timeout=0.15) # won't be precisely 100ms
46+
with pytest.raises(asyncio.exceptions.TimeoutError):
47+
await asyncio.wait_for(pm.eval("new Promise((resolve) => setTimeout(resolve, '100'))"), timeout=0.09)
48+
# Number("1 second") -> NaN -> delay turns to be 0s
49+
await asyncio.wait_for(pm.eval("new Promise((resolve) => setTimeout(resolve, '1 second'))"), timeout=0.05) # won't be precisely 0s
4250

4351
# passing an invalid ID to `clearTimeout` should silently do nothing; no exception is thrown.
4452
pm.eval("clearTimeout(NaN)")

0 commit comments

Comments
 (0)