Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Internals (client and server) of aborting an XMLHttpRequest

So I'm curious about the actual underlying behaviours that occur when aborting an async javascript request. There was some related info in this question but I've yet to find anything comprehensive.

My assumption has always been that aborting the request causes the browser to close the connection and stop processing it entirely, thus causing the server to do the same if it's been setup to do so. I imagine however that there might be browser-specific quirks or edge cases here I'm not thinking of.

My understanding is as follows, I'm hoping someone can correct it if necessary and that this can be a good reference for others going forwards.

  • Aborting the XHR request clientside causes the browser to internally close the socket and stop processing it. I would expect this behaviour rather than simply ignoring the data coming in and wasting memory. I'm not betting on IE on that though.
  • An aborted request on the server would be up to what's running there:
    • I know with PHP the default behaviour is to stop processing when the client socket is closed, unless ignore_user_abort() has been called. So closing XHR connections saves you server power as well.
    • I'm really interested to know how this could be handled in node.js, I assume some manual work would be needed there.
    • I have no idea really about other server languages / frameworks and how they behave but if anyone wants to contribute specifics I'm happy to add them here.
like image 481
pospi Avatar asked Jun 17 '14 07:06

pospi


1 Answers

For the client, the best place to look is in the source, so let's do this! :)

Let's look at Blink's implementation of XMLHttpRequest's 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's 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's 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 from this, it looks like the majority of the grunt work is done within 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'm no C++ expert but from the looks of it, internalAbort does a few things:

  • Stops any processing it's currently doing on a given incoming response
  • Clears out any internal XHR state associated with the request/response
  • Tells the inspector to report that the XHR failed (this is really interesting! I bet it's where those nice console messages originate)
  • Closes either the "legacy" version of a response stream, or the modern version of the response stream (this is probably the most interesting part pertaining to your question)
  • Deals with some threading issues to ensure the error is propagated properly (thanks, comments).

After doing a lot of digging around, I came across an interesting function within HttpResponseBodyDrainer (lines 110-124) called Finish which to me looks like something that would eventually be called when a request is cancelled:

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 the BasicHttpStream, delegates to the HttpStreamParser::Close, which, when given a non-reusable flag (which does seem to happen when the request is aborted, as seen in HttpResponseDrainer), does close the socket:

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

So, in terms 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 :) seems like most of the quirks and edge cases have to do with scheduling/event notification/threading issues, as well as browser-specific handling, e.g. reporting the aborted XHR to the devtools console.

In terms of the server, in the case of NodeJS you'd want to listen for the 'close' event on the http response object. Here's 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 running that and canceling the request before it completes. You'll see an error at your console.

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

like image 90
Travis Kaufman Avatar answered Sep 20 '22 18:09

Travis Kaufman