[webchannel] Add JavaScript client implementation#1
Conversation
There was a problem hiding this comment.
Code Review
This pull request imports the WebChannel JavaScript client implementation based on the Google Closure Library, introducing bidirectional communication over HTTP. The reviewer provided several critical code-quality and bug-fixing suggestions: resolving a potential TypeError in ChannelRequest when handling string responses or nullified XMLHTTP objects, fixing a duplicate callback bug in netUtils.testPingServer during timeouts, preventing 414 Request-URI Too Large errors in WebChannelBase by safely handling undefined map sizes, and optimizing the message queue encoding loop in WireV8.
Important
The consumer version of Gemini Code Assist on GitHub is being sunset. Starting June 18, 2026, new organization installations will be blocked, and all code review activity will officially cease on July 17, 2026.
For more details on the timeline and next steps, please review the Help Documentation.
| const responseChunks = | ||
| /** @type {!Array<!Uint8Array>|string} */ (this.xmlHttp_.getResponse()); | ||
| if (responseChunks === '') { | ||
| // in case of net::ERR_INTERNET_DISCONNECTED, this.xmlHttp_.getResponse() | ||
| // returns '' (empty string) | ||
| return ''; | ||
| } |
There was a problem hiding this comment.
If responseChunks is a non-empty string, calling textDecoder.decode(responseChunks[i]) will throw a TypeError because responseChunks[i] is a string character, not an ArrayBuffer or ArrayBufferView. It is safer to check if responseChunks is a string and handle it directly without decoding.
| const responseChunks = | |
| /** @type {!Array<!Uint8Array>|string} */ (this.xmlHttp_.getResponse()); | |
| if (responseChunks === '') { | |
| // in case of net::ERR_INTERNET_DISCONNECTED, this.xmlHttp_.getResponse() | |
| // returns '' (empty string) | |
| return ''; | |
| } | |
| const responseChunks = | |
| /** @type {!Array<!Uint8Array>|string} */ (this.xmlHttp_.getResponse()); | |
| if (typeof responseChunks === 'string') { | |
| if (responseChunks !== '') { | |
| this.fetchResponseState_.responseArrivedForFetch = true; | |
| } | |
| this.fetchResponseState_.responseBuffer += responseChunks; | |
| this.xmlHttpChunkStart_ = 0; | |
| return this.fetchResponseState_.responseBuffer; | |
| } |
| function testPingServer(url, timeout, callback) { | ||
| const channelDebug = new WebChannelDebug(); | ||
| const controller = new AbortController(); | ||
| const timeoutId = setTimeout(() => { | ||
| controller.abort(); | ||
| networkTestCallback( | ||
| channelDebug, 'TestPingServer: timeout', false, callback); | ||
| }, timeout); | ||
|
|
||
| fetch(url, {signal: controller.signal}) | ||
| .then((response) => { | ||
| clearTimeout(timeoutId); | ||
| if (response.ok) { | ||
| networkTestCallback( | ||
| channelDebug, 'TestPingServer: ok', true, callback); | ||
| } else { | ||
| networkTestCallback( | ||
| channelDebug, 'TestPingServer: server error', false, callback); | ||
| } | ||
| }) | ||
| .catch((error) => { | ||
| clearTimeout(timeoutId); | ||
| networkTestCallback( | ||
| channelDebug, 'TestPingServer: error', false, callback); | ||
| }); | ||
| } |
There was a problem hiding this comment.
In testPingServer, calling networkTestCallback inside the setTimeout handler and then calling controller.abort() causes a duplicate callback invocation. Aborting the controller rejects the fetch promise, which triggers the .catch block and invokes networkTestCallback a second time. Letting the .catch block handle the timeout abort (by checking error.name === 'AbortError') avoids this duplicate callback bug.
function testPingServer(url, timeout, callback) {
const channelDebug = new WebChannelDebug();
const controller = new AbortController();
const timeoutId = setTimeout(() => {
controller.abort();
}, timeout);
fetch(url, {signal: controller.signal})
.then((response) => {
clearTimeout(timeoutId);
if (response.ok) {
networkTestCallback(
channelDebug, 'TestPingServer: ok', true, callback);
} else {
networkTestCallback(
channelDebug, 'TestPingServer: server error', false, callback);
}
})
.catch((error) => {
clearTimeout(timeoutId);
const isTimeout = error.name === 'AbortError';
networkTestCallback(
channelDebug,
isTimeout ? 'TestPingServer: timeout' : 'TestPingServer: error',
false,
callback);
});
}| if (this.hasResponseBody_()) { | ||
| const channelRequest = this; | ||
| this.channelDebug_.dumpException(ex, function() { | ||
| return 'ResponseText: ' + channelRequest.xmlHttp_.getResponseText(); | ||
| }); |
There was a problem hiding this comment.
Evaluating channelRequest.xmlHttp_.getResponseText() inside the lazy log callback can throw a TypeError if this.xmlHttp_ has been nullified during cleanup_ (which is common on request failures). Using the local xmlhttp parameter is much safer as it is guaranteed to remain non-null.
if (this.hasResponseBody_()) {
this.channelDebug_.dumpException(ex, function() {
return 'ResponseText: ' + xmlhttp.getResponseText();
});
} else {| */ | ||
| WebChannelBase.prototype.getMaxNumMessagesForFastHandshake_ = function() { | ||
| let total = 0; | ||
| for (let i = 0; i < this.outgoingMaps_.length; i++) { | ||
| const map = this.outgoingMaps_[i]; | ||
| const size = map.getRawDataSize(); | ||
| if (size === undefined) { | ||
| break; |
There was a problem hiding this comment.
In getMaxNumMessagesForFastHandshake_, if a map has an undefined size, the loop breaks and returns this.maxMapsPerRequest_ (1000). This can result in bundling up to 1000 maps in the GET request URL, leading to 414 Request-URI Too Large errors. Returning i instead of breaking ensures we only bundle maps whose sizes we can verify.
| */ | |
| WebChannelBase.prototype.getMaxNumMessagesForFastHandshake_ = function() { | |
| let total = 0; | |
| for (let i = 0; i < this.outgoingMaps_.length; i++) { | |
| const map = this.outgoingMaps_[i]; | |
| const size = map.getRawDataSize(); | |
| if (size === undefined) { | |
| break; | |
| WebChannelBase.prototype.getMaxNumMessagesForFastHandshake_ = function() { | |
| let total = 0; | |
| for (let i = 0; i < this.outgoingMaps_.length; i++) { | |
| const map = this.outgoingMaps_[i]; | |
| const size = map.getRawDataSize(); | |
| if (size === undefined) { | |
| return i; | |
| } |
| if (mapId < 0) { | ||
| // redo the encoding in case of retry/reordering, plus extra space | ||
| offset = Math.max(0, messageQueue[i].mapId - 100); | ||
| done = false; | ||
| continue; | ||
| } |
There was a problem hiding this comment.
In encodeMessageQueue, when mapId < 0, continuing the for loop is highly inefficient because the rest of the messages are still encoded even though the buffer will be discarded and the while loop restarted. Breaking the for loop immediately avoids wasting CPU cycles.
| if (mapId < 0) { | |
| // redo the encoding in case of retry/reordering, plus extra space | |
| offset = Math.max(0, messageQueue[i].mapId - 100); | |
| done = false; | |
| continue; | |
| } | |
| if (mapId < 0) { | |
| // redo the encoding in case of retry/reordering, plus extra space | |
| offset = Math.max(0, messageQueue[i].mapId - 100); | |
| done = false; | |
| break; | |
| } |
…implemented headers
c98cd0a to
9ec52e5
Compare
@wenbozhu Hi wenbo thanks for review! Although, this is not really being merged to the main repo — just a test change for you to see. If this looks good now, I'll send this against the main repo :) |
|
Yes. Thanks. |
Sure! Opened google#69 |
9ec52e5 to
d71e0af
Compare
No description provided.