Internal (client and server) XMLHttpRequest interrupt

So, I'm curious what are the main behaviors that occur when an async javascript request is interrupted. There was some related information on this , but I have not yet found anything comprehensive.

My assumption has always been that interrupting the request causes the browser to close the connection and completely stop processing it, which caused the server to do the same if it was configured for this. I assume, however, that there may be browser-specific quirks or edge cases that I don't think about.

My understanding is this, I hope that someone can fix it if necessary, and that this can be a good reference for others going forward.

  • Canceling an XHR client request causes the browser to internally close the socket and stop processing it. I would expect such behavior, and not just ignoring the data entering and losing memory. However, I do not bet on IE.
  • An interrupted request on the server will depend on what works there:
    • I know that the default PHP is to stop processing when the client socket is closed, if ignore_user_abort() not been called. Thus, closing XHR connections also saves server power.
    • I am very interested to know how this could be handled in node.js, I assume that some manual work will be required there.
    • I have no idea what other server languages ​​/ frameworks are and how they behave, but if someone wants to contribute, I will be happy to add them here.
+6
source share
1 answer

For the client, the best place to look is in the source, so let me do it! :)

Let's look at the Blink implementation of the XMLHttpRequest abort method (lines 1083-1119 in XMLHttpRequest.cpp ):

 void XMLHttpRequest::abort() { WTF_LOG(Network, "XMLHttpRequest %p abort()", this); // internalAbort() clears |m_loader|. Compute |sendFlag| now. // // |sendFlag| corresponds to "the send() flag" defined in the XHR spec. // // |sendFlag| is only set when we have an active, asynchronous loader. // Don't use it as "the send() flag" when the XHR is in sync mode. bool sendFlag = m_loader; // internalAbort() clears the response. Save the data needed for // dispatching ProgressEvents. long long expectedLength = m_response.expectedContentLength(); long long receivedLength = m_receivedLength; if (!internalAbort()) return; // The script never gets any chance to call abort() on a sync XHR between // send() call and transition to the DONE state. It because a sync XHR // doesn't dispatch any event between them. So, if |m_async| is false, we // can skip the "request error steps" (defined in the XHR spec) without any // state check. // // FIXME: It possible open() is invoked in internalAbort() and |m_async| // becomes true by that. We should implement more reliable treatment for // nested method invocations at some point. if (m_async) { if ((m_state == OPENED && sendFlag) || m_state == HEADERS_RECEIVED || m_state == LOADING) { ASSERT(!m_loader); handleRequestError(0, EventTypeNames::abort, receivedLength, expectedLength); } } m_state = UNSENT; } 

So it looks like most of grunt's work is done inside internalAbort , which looks like this:

 bool XMLHttpRequest::internalAbort() { m_error = true; if (m_responseDocumentParser && !m_responseDocumentParser->isStopped()) m_responseDocumentParser->stopParsing(); clearVariablesForLoading(); InspectorInstrumentation::didFailXHRLoading(executionContext(), this, this); if (m_responseLegacyStream && m_state != DONE) m_responseLegacyStream->abort(); if (m_responseStream) { // When the stream is already closed (including canceled from the // user), |error| does nothing. // FIXME: Create a more specific error. m_responseStream->error(DOMException::create(!m_async && m_exceptionCode ? m_exceptionCode : AbortError, "XMLHttpRequest::abort")); } clearResponse(); clearRequest(); if (!m_loader) return true; // Cancelling the ThreadableLoader m_loader may result in calling // window.onload synchronously. If such an onload handler contains open() // call on the same XMLHttpRequest object, reentry happens. // // If, window.onload contains open() and send(), m_loader will be set to // non 0 value. So, we cannot continue the outer open(). In such case, // just abort the outer open() by returning false. RefPtr<ThreadableLoader> loader = m_loader.release(); loader->cancel(); // If abort() called internalAbort() and a nested open() ended up // clearing the error flag, but didn't send(), make sure the error // flag is still set. bool newLoadStarted = m_loader; if (!newLoadStarted) m_error = true; return !newLoadStarted; } 

I am not an expert in C ++, but from his views, internalAbort performs several actions:

  • Stops processing that is currently running on a given incoming response
  • Removes any internal XHR state associated with the request / response
  • Tells the inspector to report that XHR failed (this is really interesting! I bet where these nice console messages come up)
  • Closes either the "obsolete" version of the response stream or the modern version of the response stream (this is probably the most interesting part related to your question).
  • Contains some streaming issues to ensure the error propagates correctly (thanks, comments).

After many searches, I met an interesting function in the HttpResponseBodyDrainer (lines 110-124) called Finish , which for me looks like something that will eventually be called when the request is canceled:

 void HttpResponseBodyDrainer::Finish(int result) { DCHECK_NE(ERR_IO_PENDING, result); if (session_) session_->RemoveResponseDrainer(this); if (result < 0) { stream_->Close(true /* no keep-alive */); } else { DCHECK_EQ(OK, result); stream_->Close(false /* keep-alive */); } delete this; } 

It turns out that stream_->Close , at least in BasicHttpStream, delegates to HttpStreamParser ::Close , which, if the flag is non-reusable (which seems to happen when the request is interrupted, as shown in HttpResponseDrainer ), closes the socket:

 void HttpStreamParser::Close(bool not_reusable) { if (not_reusable && connection_->socket()) connection_->socket()->Disconnect(); connection_->Reset(); } 

So, from the point of view of what happens on the client, at least in the case of Chrome, it looks like your initial intuitions were correct, as far as I can tell :) it looks like most quirks and regional affairs have to be done with a schedule / notification about events / streams, as well as browser processing, for example reporting interrupted XHR to the devtools console.

In server terms, in the case of NodeJS, you want to listen to the close " event in the HTTP response object. Here is a simple example:

 'use strict'; var http = require('http'); var server = http.createServer(function(req, res) { res.on('close', console.error.bind(console, 'Connection terminated before response could be sent!')); setTimeout(res.end.bind(res, 'yo'), 2000); }); server.listen(8080); 

Try to start and cancel the request before it is completed. On the console you will see an error message.

Hope you found this helpful. Digging through the source of Chromium / Blink was a lot of fun :)

+10
source

Source: https://habr.com/ru/post/970925/


All Articles