Xmlhttprequest timeout error

Here is my AJAX function: /** * Send an AJAX request * * @param url The URL to call (located in the /ajax/ directory) * @param data The data to send (will be serialised with JSON) * ...

Here is my AJAX function:

/**
 * Send an AJAX request
 *
 * @param url      The URL to call (located in the /ajax/ directory)
 * @param data     The data to send (will be serialised with JSON)
 * @param callback The function to call with the response text
 * @param silent   If true, doesn't show errors to the user
 * @param loader   The element containing "Loading... Please wait"
 */
AJAX = function(url,data,callback,silent,loader) {
    var a,
        attempt = 0,
        rsc = function() {
            if( a.readyState == 4) {
                if( a.status != 200) {
                    if( a.status > 999) { // IE sometimes throws 12152
                        attempt++;
                        if( attempt < 5)
                            send();
                        else if( !silent) {
                            alert("HTTP Error "+a.status+" "+a.statusText+"<br />Failed to access "+url);
                        }
                    }
                    else if(!silent) {
                        alert("HTTP Error "+a.status+" "+a.statusText+"nFailed to access "+url);
                    }
                }
                else {
                    callback(JSON.parse(a.responseText));
                }
            }
        },
        to = function() {
            a.abort();
            attempt++;
            if( attempt < 5)
                send();
            else if( !silent) {
                alert("Request TimeoutnFailed to access "+url);
            }
        };
    data = JSON.stringify(data);
    var send = function() {
        if( loader && attempt != 0) {
            loader.children[0].firstChild.nodeValue = "Error... retrying...";
            loader.children[1].firstChild.nodeValue = "Attempt "+(attempt+1)+" of 5";
        }
        a = new XMLHttpRequest();
        a.open("POST","/ajax/"+url,true);
        a.onreadystatechange = rsc;
        a.timeout = 5000;
        a.ontimeout = to;
        a.setRequestHeader("Content-Type","application/json");
        a.send(data);
    };
    send();
};

The general idea is to attempt the request up to five times. Sometimes IE fails with an unusual HTTP error (12xxx), and sometimes the server may fail to respond.

The problem I’m having is that the abort() call doesn’t appear to be aborting the connection. To test, I made a simple PHP script:

<?php
    sleep(60);
    touch("test/".uniqid());
    die("Request completed.");
?>

The touch() call creates a file with the current uniqid() — by looking at the modification time I can see the time the sleep(60) ended.

Expected behaviour:

The request is sent
After five seconds, the text changes to «Error… Retying… Attempt 2/5»
Repeat the above up until Attempt 5/5, then fail.
The five calls to the PHP file are aborted, and either there will be five files in the «test» folder, spaced 5 seconds apart, or there will be none because ignore_user_abort is off.

Observed behaviour (in IE9):

The request is sent
The attempt text appears and changes as it should
After five attempts, the error message is displayed
I am unable to load any pages for five whole minutes.
On the server, there are five files spaced one minute apart

I don’t know what to make of this, because on the server side Requests 3, 4 and 5 are being sent minutes after the «Timeout» error message is shown on the browser.

If it makes any difference, the page making the AJAX calls is in an iframe. Reloading the iframe (using iframe.contentWindow.location.reload() does NOT fix the issue, it still waits for those five requests to go through.

Why is this happening? How can I fix it?

EDIT: I’ve run the test again using Developer Tools to monitor network activity. The result is:

URL          Method   Result     Type   Received  Taken   Initiator
/ajax/testto          (Aborted)              0 B  < 1 ms (Pending...)
/ajax/testto          (Aborted)              0 B  125 ms (Pending...)
/ajax/testto          (Aborted)              0 B  125 ms (Pending...)
/ajax/testto          (Aborted)              0 B  125 ms (Pending...)
/ajax/testto          (Aborted)              0 B  124 ms (Pending...)


Abstract

The XMLHttpRequest specification defines an API that provides scripted
client functionality for transferring data between a client and a server.

Status of this Document

This section describes the status of this document at the time of its
publication. Other documents may supersede this document. A list of current
W3C publications and the latest revision of this technical report can be
found in the W3C technical reports index
at http://www.w3.org/TR/.

This document is published as a snapshot of the
XMLHttpRequest Living Specification.

If you wish to make comments regarding this document in a manner
that is tracked by the W3C, please submit them via using our public bug database, or please send comments to
public-webapps@w3.org
(archived)
with [XHR] at the start of the subject line.

The W3C Web Applications Working
Group is the W3C working group responsible for this specification’s progress along the W3C Recommendation track. This specification is the 30 January 2014 Working Draft.

Publication as a Working Draft does not imply endorsement by the W3C
Membership. This is a draft document and may be updated, replaced or
obsoleted by other documents at any time. It is inappropriate to cite this
document as other than work in progress.

Work on this specification is also done at the WHATWG. The W3C Web Applications working group actively pursues convergence of XMLHttpRequest specification with the WHATWG.

This document was produced by a group operating under the
5 February 2004
W3C Patent Policy. W3C maintains a
public
list of any patent disclosures made in connection with the deliverables of
the group; that page also includes instructions for disclosing a patent. An
individual who has actual knowledge of a patent which the individual believes
contains
Essential
Claim(s) must disclose the information in accordance with
section
6 of the W3C Patent Policy.

Table of Contents

  1. 1 Introduction
    1. 1.1 Specification history
  2. 2 Conformance
    1. 2.1 Dependencies
    2. 2.2 Extensibility
  3. 3 Terminology
  4. 4 Interface XMLHttpRequest
    1. 4.1 Task sources
    2. 4.2 Constructor
    3. 4.3 Garbage collection
    4. 4.4 Event handlers
    5. 4.5 States
    6. 4.6 Request
      1. 4.6.1 The open() method
      2. 4.6.2 The setRequestHeader() method
      3. 4.6.3 The timeout attribute
      4. 4.6.4 The withCredentials attribute
      5. 4.6.5 The upload attribute
      6. 4.6.6 The send() method
      7. 4.6.7 Infrastructure for the send() method
      8. 4.6.8 The abort() method
    7. 4.7 Response
      1. 4.7.1 The status attribute
      2. 4.7.2 The statusText attribute
      3. 4.7.3 The getResponseHeader() method
      4. 4.7.4 The getAllResponseHeaders() method
      5. 4.7.5 Response entity body
      6. 4.7.6 The overrideMimeType() method
      7. 4.7.7 The responseType attribute
      8. 4.7.8 The response attribute
      9. 4.7.9 The responseText attribute
      10. 4.7.10 The responseXML attribute
    8. 4.8 Events summary
  5. 5 Interface FormData
  6. References
  7. Acknowledgments

1 Introduction

This section is non-normative.

The XMLHttpRequest object is an API for
fetching resources.

The name of the object is XMLHttpRequest for compatibility
with the Web, though each component of this name is potentially
misleading. First, the object supports any text based format, including
XML. Second, it can be used to make requests over both HTTP and HTTPS
(some implementations support protocols in addition to HTTP and HTTPS, but
that functionality is not covered by this specification). Finally, it
supports «requests» in a broad sense of the term as it pertains to HTTP;
namely all activity involved with HTTP requests or responses for the
defined HTTP methods.

Some simple code to do something with data from an XML document
fetched over the network:

function processData(data) {
  // taking care of data
}

function handler() {
  if(this.readyState == this.DONE) {
    if(this.status == 200 &&
       this.responseXML != null &&
       this.responseXML.getElementById('test').textContent) {
      // success!
      processData(this.responseXML.getElementById('test').textContent);
      return;
    }
    // something went wrong
    processData(null);
  }
}

var client = new XMLHttpRequest();
client.onreadystatechange = handler;
client.open("GET", "unicorn.xml");
client.send();

If you just want to log a message to the server:

function log(message) {
  var client = new XMLHttpRequest();
  client.open("POST", "/log");
  client.setRequestHeader("Content-Type", "text/plain;charset=UTF-8");
  client.send(message);
}

Or if you want to check the status of a document on the server:

function fetchStatus(address) {
  var client = new XMLHttpRequest();
  client.onreadystatechange = function() {
    // in case of network errors this might not give reliable results
    if(this.readyState == this.DONE)
      returnStatus(this.status);
  }
  client.open("HEAD", address);
  client.send();
}

1.1 Specification history

The XMLHttpRequest object was initially defined as part of
the WHATWG’s HTML effort. (Long after Microsoft shipped an implementation.)
It moved to the W3C in 2006. Extensions (e.g. progress events and
cross-origin requests) to XMLHttpRequest were developed in a
separate draft (XMLHttpRequest Level 2) until end of 2011, at which point
the two drafts were merged and XMLHttpRequest became a single
entity again from a standards perspective. Since 2012, the development work
required for getting the spec finalized has taken place both in the WHATWG and
in the W3C Web Applications working group.

XMLHttpRequest Level 1, the first stable Recommendation track
specification for the XMLHttpRequest feature, standardizes all
parts of XMLHttpRequest that are compatibly supported across
major implementations. Implementors should be able to rely on this
specification and the
related test suite
in order to create interoperable implementations.

Some features included in the
WHATWG specification are left out
because they are not yet widely implemented or used. These features are:

  • Fetching
    data: URLs.
  • The URLSearchParams type in send() method.
  • The additional methods other than append() defined in the interface FormData.

Historical discussion can be found in the following mailing list
archives:

  • whatwg@whatwg.org
  • public-webapi@w3.org
  • public-appformats@w3.org
  • public-webapps@w3.org

2 Conformance

All diagrams, examples, and notes in this specification are
non-normative, as are all sections explicitly marked non-normative.
Everything else in this specification is normative.

The key words «MUST», «MUST NOT», «REQUIRED», «SHOULD», «SHOULD NOT», «RECOMMENDED», «MAY», and
«OPTIONAL» in the normative parts of this specification are to be
interpreted as described in RFC2119. For readability, these words do
not appear in all uppercase letters in this specification.
[RFC2119]

2.1 Dependencies

This specification relies on several underlying specifications.

Cross-Origin Resource Sharing

A conforming user agent must
support the algorithms of the Cross-Origin Resource Sharing
specification. [CORS]

DOM4

A conforming user agent must
support at least the subset of the functionality defined in DOM4 that
this specification relies upon, such as various exceptions and
EventTarget. [DOM]

DOM Parsing and Serialization

A conforming user agent must support at least the
serialize
concept from DOM Parsing and Serialization.
[DOMPS]

Encoding Standard

A conforming user agent must
support at least the subset of the functionality defined in Encoding Standard that
this specification relies upon, such as the utf-8 encoding.
[ENCODING]

File API

A conforming user agent must
support at least the subset of the functionality defined in File API that
this specification relies upon, such as the Blob and
File interfaces. [FILEAPI]

HTML

A conforming user agent must
support at least the subset of the functionality defined in HTML that
this specification relies upon, such as the basics of the
Window object and serializing a Document
object. [HTML]

HTTP

A conforming user agent must
support some version of the HTTP protocol. Requirements regarding HTTP
are made throughout the specification. [HTTP]

Progress Events

A conforming user agent must support the
Progress Events specification.
[PROGRESSEVENTS]

Typed Array

A conforming user agent must support the
ArrayBuffer and
ArrayBufferView objects.
[TYPEDARRAY]

URL

A conforming user agent must
support the URL parsing algorithm of the URL
specification. [URL]

Web IDL

A conforming user agent must also
be a conforming implementation of the IDL fragments in this
specification, as described in the Web IDL specification.
[WEBIDL]

XML

A conforming user agent must be a
conforming XML processor that reports violations of namespace
well-formedness. [XML]
[XMLNS]

It uses the typographic conventions from HTML. [HTML]

2.2 Extensibility

User agents, Working Groups, and other interested parties are
strongly encouraged to discuss new features on a relevant public
forum, preferably
public-webapps@w3.org. If this
is for some reason not possible prefix the extension in some way. E.g. if
company Foo wants to add a proprietary method bar() it could
be named fooBar() to prevent clashes with a potential
non-proprietary method bar().

3 Terminology

The term user credentials for the purposes of this
specification means cookies, HTTP authentication, and client-side SSL
certificates. Specifically it does not refer to proxy authentication or
the Origin header.
[COOKIES]

4 Interface XMLHttpRequest

[NoInterfaceObject]
interface XMLHttpRequestEventTarget : EventTarget {
  // event handlers
  attribute EventHandler onloadstart;
  attribute EventHandler onprogress;
  attribute EventHandler onabort;
  attribute EventHandler onerror;
  attribute EventHandler onload;
  attribute EventHandler ontimeout;
  attribute EventHandler onloadend;
};

interface XMLHttpRequestUpload : XMLHttpRequestEventTarget {

};

enum XMLHttpRequestResponseType {
  "",
  "arraybuffer",
  "blob",
  "document",
  "json",
  "text"
};

[Constructor]
interface XMLHttpRequest : XMLHttpRequestEventTarget {
  // event handler
  attribute EventHandler onreadystatechange;

  // states
  const unsigned short UNSENT = 0;
  const unsigned short OPENED = 1;
  const unsigned short HEADERS_RECEIVED = 2;
  const unsigned short LOADING = 3;
  const unsigned short DONE = 4;
  readonly attribute unsigned short readyState;

  // request
  void open(ByteString method, [EnsureUTF16] DOMString url);
  void open(ByteString method, [EnsureUTF16] DOMString url, boolean async, optional [EnsureUTF16] DOMString? username = null, optional [EnsureUTF16] DOMString? password = null);
  void setRequestHeader(ByteString header, ByteString value);
           attribute unsigned long timeout;
           attribute boolean withCredentials;
  readonly attribute XMLHttpRequestUpload upload;
  void send(optional (ArrayBufferView or Blob or Document or [EnsureUTF16] DOMString or FormData)? data = null);
  void abort();

  // response
  readonly attribute unsigned short status;
  readonly attribute ByteString statusText;
  ByteString? getResponseHeader(ByteString header);
  ByteString getAllResponseHeaders();
  void overrideMimeType(DOMString mime);
           attribute XMLHttpRequestResponseType responseType;
  readonly attribute any response;
  readonly attribute DOMString responseText;
  readonly attribute Document? responseXML;
};

Each XMLHttpRequest object has a unique, associated
XMLHttpRequestUpload object.

If the JavaScript global environment is a
worker environment, implementations must act as if
Document and Document? in the above IDL were not
exposed. I.e. send() is not overloaded with it
and responseXML always returns null (as
required by its definition, too).

4.1 Task sources

Each XMLHttpRequest object has its own
task source. Namely, the
XMLHttpRequest task source.

4.2 Constructor

The XMLHttpRequest object has an associated
settings object.

client = new XMLHttpRequest()
Returns a new XMLHttpRequest object.

The
XMLHttpRequest()
constructor must run these steps:

  1. Let xhr be a new XMLHttpRequest
    object.

  2. Set xhr‘s
    settings object to the
    relevant settings object
    for the global object of xhr‘s interface object.

  3. Return xhr.

4.3 Garbage collection

An XMLHttpRequest object must not be garbage collected if
its state is OPENED and the
send() flag is set, its state is
HEADERS_RECEIVED, or
its state is LOADING, and
one of the following is true:

  • It has one or more
    event listeners
    registered whose type is
    readystatechange,
    progress,
    abort,
    error,
    load,
    timeout, or
    loadend.

  • The upload complete flag is unset and the associated
    XMLHttpRequestUpload object has one or more
    event listeners
    registered whose type is
    progress,
    abort,
    error,
    load,
    timeout, or
    loadend.

If an XMLHttpRequest object is garbage collected while its
connection is still open, the user agent must terminate the request.

4.4 Event handlers

The following are the
event handlers (and their corresponding
event handler event types)
that must be supported on objects implementing an interface that inherits
from XMLHttpRequestEventTarget as attributes:

event handler event handler event type
onloadstart loadstart
onprogress progress
onabort abort
onerror error
onload load
ontimeout timeout
onloadend loadend

The following is the
event handler
(and its corresponding
event handler event type) that must be
supported as attribute solely by the
XMLHttpRequest object:

event handler event handler event type
onreadystatechange readystatechange

4.5 States

client . readyState

Returns the current state.

The XMLHttpRequest object can be in several states. The
readyState
attribute must return the current state, which must be one of the
following values:

UNSENT
(numeric value 0)

The object has been constructed.

OPENED
(numeric value 1)

The open() method has been successfully invoked.
During this state request headers can be set using
setRequestHeader()
and the request can be made using the
send() method.

(numeric value 2)

All redirects (if any) have been followed and all HTTP headers of
the final response have been received. Several response members of the
object are now available.

LOADING
(numeric value 3)

The response entity body is being received.

DONE
(numeric value 4)

The data transfer has been completed or something went wrong
during the transfer (e.g. infinite redirects).

The send() flag indicates
that the send() method has
been invoked. It is initially unset and is used during the
OPENED state.

The error flag indicates some type of
network error or fetch termination. It is initially unset.

4.6 Request

Each XMLHttpRequest object has the following
request-associated concepts:

request method,
request URL,
,
request entity body,
source origin,
referrer source,
synchronous flag,
upload complete flag, and
upload events flag.

The author request headers is a list of HTTP header names
and corresponding header values. Comparisons against the HTTP header names
must be done in a case-insensitive manner. Initially it must be empty.

The request entity body must initially be null.

The synchronous flag,
upload complete flag, and
upload events flag must be initially unset.


To terminate the request run these steps:

  1. Set the error flag.

  2. Cancel any instance of the fetch algorithm
    opened by this object.

  3. If there are any tasks from
    the object’s XMLHttpRequest task source in one of the
    task queues, then remove them.

4.6.1 The open() method

client . open(method, url [, async = true [, username = null [, password = null]]])

Sets the request method, request URL, and
synchronous flag.

Throws a JavaScript TypeError if
either method is not a valid HTTP method or
url cannot be parsed.

Throws a «SecurityError» exception
if method is a case-insensitive match for
CONNECT, TRACE or TRACK.

Throws an «InvalidAccessError»
exception if async is false, the
JavaScript global environment is a
document environment, and either the
timeout attribute is not
zero, the
withCredentials
attribute is true, or the
responseType
attribute is not the empty string.

The
open(method, url, async, username, password)
method must run these steps:

  1. If settings object’s
    responsible document is not
    fully active,
    throw an
    «InvalidStateError» exception.

  2. Set base to
    settings object’s
    API base URL.

  3. Set source origin to
    settings object’s
    origin.

  4. Set referrer source to the
    settings object’s
    API referrer source’s
    URL if
    settings object’s
    API referrer source is a
    document, and
    settings object’s
    API referrer source otherwise.

  5. If method does not match the Method
    token production, throw a JavaScript TypeError.

  6. If method is a case-insensitive match for CONNECT,
    DELETE, GET, HEAD, OPTIONS,
    POST, PUT, TRACE, or TRACK, subtract
    0x20 from each byte in the range 0x61 (ASCII a) to 0x7A (ASCII z).

    If it does not match any of the above, it is passed
    through literally, including in the final request.

  7. If method is a case-sensitive match for CONNECT,
    TRACE, or TRACK,
    throw a
    «SecurityError» exception.

    Allowing these methods would pose a security risk.
    [HTTPVERBSEC]

  8. Let parsed URL be the result of
    parsing url
    with base.

  9. If parsed URL is failure,
    throw a JavaScript TypeError.

  10. If the async argument is omitted, set async to true, and set
    username and password to null.

    Due to unfortunate legacy constraints, passing
    undefined for the async argument is treated differently
    from async being omitted.

  11. If parsed URL‘s relative flag is
    set, run these substeps:

    1. If the username argument is not null, set
      parsed URL‘s
      username to
      username.

    2. If the password argument is not null, set
      parsed URL‘s
      password to
      password.

  12. If async is false, the
    JavaScript global environment is a
    document environment, and either the
    timeout attribute value is not zero, the
    withCredentials attribute value is
    true, or the responseType attribute
    value is not the empty string,
    throw an
    «InvalidAccessError» exception.

  13. Terminate the request.

    After all, a request can be ongoing at this point.

  14. Set variables associated with the object as follows:

    • Set request method to method.

    • Set request URL to parsed URL.

    • If async is false, set the synchronous flag.

    • Set author request headers to the empty list.

    • Unset the send() flag.

    • Set response entity body to null.

    • Set arraybuffer response entity body to null.

    • Set blob response entity body to null.

    • Set document response entity body to null.

    • Set JSON response entity body to null.

    • Set text response entity body to null.

  15. If the state is not OPENED, run these
    substeps:

    1. Change the state to OPENED.

    2. Fire an event named readystatechange.

client . setRequestHeader(header, value)

Appends an header to the list of
author request headers, or if header is already
in the list of author request headers, combines its value
with value.

Throws an «InvalidStateError»
exception if the state is not
OPENED or if the
send() flag is set.

Throws a JavaScript TypeError if
header is not a valid HTTP header field name or if
value is not a valid HTTP header field value.

As indicated in the algorithm below certain headers cannot
be set and are left up to the user agent. In addition there are certain
other headers the user agent will take control of if they are not set by
the author as indicated at the end of the
send() method section.

For non same origin requests using the HTTP
GET method a preflight request is made when headers other
than Accept and Accept-Language are set.

The

method must run these steps:

  1. If the state is not
    OPENED,
    throw an
    «InvalidStateError» exception.

  2. If the send() flag is set,
    throw an
    «InvalidStateError» exception.

  3. If header does not match the
    field-name production,
    throw a JavaScript TypeError.

  4. If value does not match the
    field-value production,
    throw a JavaScript TypeError.

    An empty string represents an empty header field value.

  5. Terminate these steps if header is a case-insensitive
    match for one of the following headers:

    • Accept-Charset
    • Accept-Encoding
    • Access-Control-Request-Headers
    • Access-Control-Request-Method
    • Connection
    • Content-Length
    • Cookie
    • Cookie2
    • Date
    • DNT
    • Expect
    • Host
    • Keep-Alive
    • Origin
    • Referer
    • TE
    • Trailer
    • Transfer-Encoding
    • Upgrade
    • User-Agent
    • Via

    … or if the start of header is a case-insensitive
    match for Proxy- or Sec- (including when
    header is just Proxy- or Sec-).

    The above headers are controlled by the user agent to
    let it control those aspects of transport. This guarantees data
    integrity to some extent. Header names starting with Sec-
    are not allowed to be set to allow new headers to be minted that are
    guaranteed not to come from XMLHttpRequest.

  6. If header is not in the
    author request headers list, append header with
    its associated value to the list and terminate these
    steps.

  7. If header is in the author request headers list, append
    «,«, followed by U+0020, followed by value, to the
    value of the header matching header.

    The XMLHttpRequest standard intentionally constraints the
    use of HTTP here in line with contemporary implementations.

Some simple code demonstrating what happens when setting the same
header twice:

// The following script:
var client = new XMLHttpRequest();
client.open('GET', 'demo.cgi');
client.setRequestHeader('X-Test', 'one');
client.setRequestHeader('X-Test', 'two');
client.send();

// …results in the following header being sent:
X-Test: one, two

4.6.3 The timeout attribute

client . timeout

Can be set to a time in milliseconds. When set to a non-zero value
will cause fetching to
terminate after the given time has passed. When the time has passed, the request has
not yet completed, and the synchronous flag is unset, a
timeout event will then be
dispatched,
or a «TimeoutError» exception will be
thrown otherwise
(for the send() method).

When set: throws an
«InvalidAccessError» exception if
the synchronous flag is set and the
JavaScript global environment is a
document environment.

The
timeout
attribute must return its value. Initially its value must be zero.

Setting the timeout
attribute must run these steps:

  1. If the
    JavaScript global environment is a
    document environment and the
    synchronous flag is set,
    throw an
    «InvalidAccessError» exception.

  2. Set its value to the new value.

This implies that the
timeout attribute can be
set while fetching is in
progress. If that occurs it will still be measured relative to the start
of fetching.

4.6.4 The withCredentials attribute

client . withCredentials

True when user credentials are to be included in a
cross-origin request. False when they are to be excluded in a
cross-origin request and when cookies are to be ignored in its response.
Initially false.

When set: throws an
«InvalidStateError» exception if the
state is not UNSENT or
OPENED, or if
the send() flag is set.

When set: throws an
«InvalidAccessError» exception if
the synchronous flag is set and the
JavaScript global environment is a
document environment.

The
withCredentials
attribute must return its value. Initially its value must be false.

Setting the
withCredentials
attribute must run these steps:

  1. If the state is not
    UNSENT or
    OPENED,
    throw an
    «InvalidStateError» exception.

  2. If the send() flag is set,
    throw an
    «InvalidStateError» exception.

  3. If the
    JavaScript global environment is a
    document environment and the
    synchronous flag is set,
    throw an
    «InvalidAccessError» exception.

  4. Set the
    withCredentials
    attribute’s value to the given value.

The
withCredentials
attribute has no effect when
fetching
same-origin
resources.

4.6.5 The upload attribute

client . upload

Returns the associated XMLHttpRequestUpload
object. It can be used to gather transmission information when data is
transferred to a server.

The
upload
attribute must return the associated
XMLHttpRequestUpload object.

As indicated earlier, each XMLHttpRequest
object has an associated XMLHttpRequestUpload object.

4.6.6 The send() method

client . send([data = null])

Initiates the request. The optional argument provides the
request entity body. The argument is ignored if
request method is GET or
HEAD.

Throws an «InvalidStateError»
exception if the state is not
OPENED or if the
send() flag is set.

The send(data)
method must run these steps:

  1. If the state is not
    OPENED,
    throw an
    «InvalidStateError» exception.

  2. If the send() flag is set,
    throw an
    «InvalidStateError» exception.

  3. If the request method is GET or
    HEAD, set data to null.

  4. If data is null, do not include a
    request entity body and go to the next step.

    Otherwise, let encoding be null, mime type be
    null, and then follow these rules, depending on data:

    ArrayBufferView

    Let the request entity body be the raw data
    represented by data.

    Blob

    If the object’s
    type
    attribute is not the empty string let mime type be its
    value.

    Let the request entity body be the raw data
    represented by data.

    document

    Let encoding be «UTF-8«.

    If data is an HTML document, let
    mime type be «text/html«, or let mime type be
    «application/xml» otherwise. Then append «;charset=UTF-8» to
    mime type.

    Let the request entity body be data,
    serialized,
    converted to Unicode,
    and utf-8 encoded.
    Re-throw any exception
    serializing throws.

    If data cannot be
    serialized, an
    «InvalidStateError» exception is thrown.

    a string

    Let encoding be «UTF-8«.

    Let mime type be «text/plain;charset=UTF-8«.

    Let the request entity body be data,
    utf-8 encoded.

    FormData

    Let the request entity body be the result of running
    the
    multipart/form-data encoding algorithm
    with data as form data set and with
    utf-8 as the
    explicit character encoding.

    Let mime type be the concatenation of
    «multipart/form-data;«,
    a U+0020 SPACE character,
    «boundary=«, and the
    multipart/form-data boundary string
    generated by the
    multipart/form-data encoding algorithm.

    If a Content-Type header is in
    author request headers and its value is a
    valid MIME type that has a
    charset parameter whose value is not a case-insensitive
    match for encoding, and encoding
    is not null, set all the charset parameters of that
    Content-Type header to encoding.

    If no Content-Type header is in
    author request headers and mime type is
    not null, append a Content-Type header with value
    mime type to author request headers.

  5. If the synchronous flag is set, release the
    storage mutex.

  6. Unset the error flag,
    upload complete flag and upload events flag.

  7. If there is no request entity body or if it is empty,
    set the upload complete flag.

  8. If the synchronous flag is unset and one or more
    event listeners are registered on the XMLHttpRequestUpload
    object, set the upload events flag.

  9. If the synchronous flag is unset, run these substeps:

    1. Set the send() flag.

    2. Fire a progress event named loadstart.

    3. If the upload complete flag is unset,
      fire a progress event named loadstart
      on the XMLHttpRequestUpload object.

    4. Return the send()
      method call, but continue running the steps in this algorithm.

  10. If the source origin and the request URL
    are same origin

    These are the same-origin request steps.

    Fetch the
    request URL from origin
    source origin, using
    referrer source as
    override referrer source, with the
    synchronous flag set if the
    synchronous flag is set, using HTTP method
    request method, taking into account the
    request entity body, list of
    author request headers, and the rules listed at the end of
    this section.

    If the synchronous flag is set

    While making the request also follow the
    same-origin request event rules.

    The
    send() method call will
    now be returned by virtue of this algorithm ending.

    If the synchronous flag is unset

    Make upload progress notifications.

    Make progress notifications.

    While processing the request, as data becomes available and when
    the user interferes with the request,
    queue tasks
    to update the response entity body and follow the
    same-origin request event rules.

    Otherwise

    These are the cross-origin request steps.

    Make a cross-origin request,
    passing these as parameters:

    request URL
    The request URL.
    request method
    The request method.
    author request headers
    The list of author request headers.
    request entity body
    The request entity body.
    source origin
    The source origin.
    referrer source
    The
    referrer source.

    omit credentials flag
    Set if
    withCredentials
    attribute’s value is false.

    force preflight flag
    Set if the upload events flag is set.
    If the synchronous flag is set

    While making the request also follow the
    cross-origin request event rules.

    The
    send() method call will
    now be returned by virtue of this algorithm ending.

    If the synchronous flag is unset

    While processing the request, as data becomes available and when
    the end user interferes with the request,
    queue tasks to update the
    response entity body and follow the
    cross-origin request event rules.


If the user agent allows the end user to configure a proxy it
should modify the request appropriately; i.e., connect
to the proxy host instead of the origin server, modify the
Request-Line and send Proxy-Authorization
headers as specified.


If the user agent supports HTTP Authentication and
Authorization is not in the list
of author request headers, it should
consider requests originating from the XMLHttpRequest object
to be part of the protection space that includes the accessed URIs and
send Authorization headers and
handle 401 Unauthorized requests appropriately.

If authentication fails,
source origin and the
request URL are same origin,
Authorization is not in the list
of author request headers,
request URL’s
username is
the empty string and request URL’s
password is
null, user agents should prompt the end user for their username and
password.

Otherwise, if authentication fails, user agents
must not prompt the end user for their username and
password. [HTTPAUTH]

Unfortunately end users are prompted because of legacy
content constraints. However, when possible this behavior is prohibited,
as it is bad UI. E.g. that is why the
same origin restriction is made above.


If the user agent supports HTTP State Management it
should persist, discard and send cookies (as received
in the Set-Cookie response header, and sent in the
Cookie header) as applicable.
[COOKIES]


If the user agent implements a HTTP cache it should
respect Cache-Control headers in
author request headers
(e.g. Cache-Control: no-cache bypasses the cache). It
must not send Cache-Control or
Pragma request headers automatically unless the end user
explicitly requests such behavior (e.g. by reloading the page).

For 304 Not Modified responses that are a result of a
user agent generated conditional request the user agent
must act as if the server gave a 200 OK
response with the appropriate content. The user agent
must allow author request headers to override automatic cache
validation (e.g. If-None-Match or
If-Modified-Since), in which case
304 Not Modified responses must be passed through.
[HTTP]


If the user agent implements server-driven content-negotiation
it must follow these constraints for the
Accept and Accept-Language request headers:

  • Both headers must not be modified if they are in
    author request headers.

  • If not in author request headers,
    Accept-Language with an appropriate value should be appended
    to it.

  • If not in author request headers, Accept
    with value */* must be appended to it.

Responses must have the content-encodings
automatically decoded. [HTTP]


Besides the author request headers, user agents
should not include additional request headers other than those mentioned
above or other than those authors are not allowed to set using
setRequestHeader().
This ensures that authors have a predictable API.

4.6.7 Infrastructure for the send() method

The same-origin request event rules are as follows:

If the error flag is set

Terminate these steps.

If the response has an HTTP status code of 301, 302, 303, 307, or 308

If the redirect violates infinite loop precautions this is a
network error.

Otherwise, run these steps:

  1. Set the request URL to the
    URL conveyed by the
    Location header.

  2. If the source origin and the
    origin of request URL
    are same origin transparently follow
    the redirect while observing the
    same-origin request event rules.

  3. Otherwise, follow the cross-origin request steps
    and terminate the steps for this algorithm.

HTTP places requirements on the user agent regarding the
preservation of the request method and
request entity body during redirects, and also requires end
users to be notified of certain kinds of automatic redirections.

If the end user cancels the request

This is an abort error.

If there is a network error

In case of DNS errors, TLS negotiation failure, or other type of
network errors, this is a network error. Do not request any
kind of end user interaction.

This does not include HTTP responses that indicate
some type of error, such as HTTP status code 410.

If timeout is not 0
and since the request started the amount of milliseconds specified by
timeout has passed

This is a timeout error.

Once all HTTP headers have been received, the
synchronous flag is unset, and the HTTP status code of the
response is not one of 301, 302, 303, 307, and 308

Switch to the HEADERS_RECEIVED state.

Once the first byte (or more) of the
response entity body has been received and the
synchronous flag is unset
If there is no response entity body and the
synchronous flag is unset

Switch to the LOADING state.

Once the whole response entity body has been
received
If there is no response entity body and the state is
LOADING
If there is no response entity body and the
synchronous flag is set

Switch to the DONE state.


The cross-origin request event rules are as follows:

If the error flag is set

Terminate these steps.

If the cross-origin request status
is preflight complete and the synchronous flag is
unset

Make upload progress notifications.

If the cross-origin request status
is network error

This is a network error.

If the cross-origin request status
is abort error

This is an abort error.

If timeout is not 0
and since the request started the amount of milliseconds specified by
timeout has passed

This is a timeout error.

Once all HTTP headers have been received, the
cross-origin request status is
success, and the synchronous flag is unset

Switch to the HEADERS_RECEIVED state.

Make progress notifications.

Once the first byte (or more) of the
response entity body has been received, the
cross-origin request status is
success, and the synchronous flag is unset
If there is no response entity body, the
cross-origin request status is
success, and the synchronous flag is unset

Switch to the LOADING state.

Once the whole response entity body has been received
and the cross-origin request status is
success
If there is no response entity body, the
cross-origin request status is
success, and the state is
LOADING
If there is no response entity body, the
cross-origin request status is
success, and the synchronous flag is set

Switch to the DONE state.


When something is said to be a network error run the
request error steps for exception
«NetworkError» and
event error.

When something is said to be an abort error run the
request error steps for exception
«AbortError» and event
abort.

When something is said to be a timeout error run the
request error steps for exception
«TimeoutError» and event
timeout.

When something is said to be a request error for
exception exception and event event run these
steps:

  1. Terminate the request.

  2. Change the state to DONE.

  3. If the synchronous flag is set,
    throw an
    exception exception.

  4. Fire an event named readystatechange.

    At this point it is clear that the
    synchronous flag is unset.

  5. If the upload complete flag is unset, follow these
    substeps:

    1. Set the upload complete flag.

    2. Fire a progress event named
      progress on the XMLHttpRequestUpload object.

    3. Fire a progress event named
      event on the XMLHttpRequestUpload object.

    4. Fire a progress event named
      loadend on the XMLHttpRequestUpload object.

  6. Fire a progress event named progress.

  7. Fire a progress event named event.

  8. Fire a progress event named loadend.


When it is said to

run these steps:

  1. Change the state to HEADERS_RECEIVED.

  2. Fire an event named readystatechange.

When it is said to
switch to the LOADING state run these
steps:

  1. Change the state to LOADING.

  2. Fire an event named readystatechange.

When it is said to
switch to the DONE state run these steps:

  1. If the synchronous flag is set, update the
    response entity body.

  2. Unset the synchronous flag.

  3. Change the state to DONE.

  4. Fire an event named readystatechange.

  5. Fire a progress event named progress.

  6. Fire a progress event named load.

  7. Fire a progress event named loadend.


When it is said to make progress notifications, while the
download is progressing, queue a task to
fire a progress event named progress
about every 50ms or for every byte received, whichever is least
frequent.


When it is said to make upload progress notifications run
these steps:

  • While the request entity body is being transmitted and the
    upload complete flag is unset,
    queue a task to
    fire a progress event named progress on
    the XMLHttpRequestUpload object about every 50ms or for
    every byte transmitted, whichever is least frequent.

  • If the request entity body has been fully transmitted
    (irrespective of whether the server has started transmitting a response
    or the status code of such a response) and the
    upload complete flag is still unset,
    queue a task to run these substeps:

    1. Set the upload complete flag.

    2. Fire a progress event named progress
      on the XMLHttpRequestUpload object.

    3. Fire a progress event named load
      on the XMLHttpRequestUpload object.

    4. Fire a progress event named loadend
      on the XMLHttpRequestUpload object.

4.6.8 The abort() method

client . abort()
Cancels any network activity.

The abort() method must run
these steps:

  1. Terminate the request.

  2. If the state is UNSENT,
    OPENED with the
    send() flag being unset, or
    DONE go to the next step.

    Otherwise, run these substeps:

    1. Change the state to DONE.

    2. Unset the send() flag.

    3. Fire an event named readystatechange.

    4. If the upload complete flag is false run these
      substeps:

      1. Set the upload complete flag to true.

      2. Fire a progress event named progress
        on the XMLHttpRequestUpload object.

      3. Fire a progress event named abort
        on the XMLHttpRequestUpload object.

      4. Fire a progress event named loadend
        on the XMLHttpRequestUpload object.

    5. Fire a progress event named progress.

    6. Fire a progress event named abort.

    7. Fire a progress event named loadend.

  3. Change the state to UNSENT.

    No readystatechange event is dispatched.

4.7 Response

A is a HTTP response header
transmitted before the response entity body.
[HTTP]

This excludes trailer fields («trailers»).

4.7.1 The status attribute

client . status

Returns the HTTP status code.

The
status
attribute must return the result of running these
steps:

  1. If the state is
    UNSENT or
    OPENED, return 0.

  2. If the error flag is set, return 0.

  3. Return the HTTP status code.

4.7.2 The statusText attribute

client . statusText

Returns the HTTP status text.

The
statusText
attribute must return the result of running these steps:

  1. If the state is
    UNSENT or
    OPENED, return the empty string.

  2. If the error flag is set, return the empty string.

  3. Return the HTTP status text.

client . getResponseHeader(header)

Returns the header field value from the response of which the
field name matches header, unless the field name is
Set-Cookie or Set-Cookie2.

The

method must run these steps:

  1. If the state is
    UNSENT or
    OPENED, return null.

  2. If the error flag is set, return null.

  3. If header is a case-insensitive match for
    Set-Cookie or Set-Cookie2, return null.

  4. If header is a case-insensitive match for multiple
    response headers, return the values of these
    headers as a single concatenated string separated from each other by a
    U+002C COMMA U+0020 SPACE character pair.

  5. If header is a case-insensitive match for a single
    response header, return the value of that header.

  6. Return null.

The Cross-Origin Resource Sharing specification filters
response headers exposed by
getResponseHeader()
for cross-origin requests.
[CORS]

For the following script:

var client = new XMLHttpRequest();
client.open("GET", "unicorns-are-teh-awesome.txt", true);
client.send();
client.onreadystatechange = function() {
  if(this.readyState == 2) {
    print(client.getResponseHeader("Content-Type"));
  }
}

The print() function will get to process something
like:

text/plain; charset=UTF-8

client . getAllResponseHeaders()

Returns all headers from the response, with the exception of those
whose field name is Set-Cookie or
Set-Cookie2.

The

method must run these steps:

  1. If the state is
    UNSENT or
    OPENED, return the empty string.

  2. If the error flag is set, return the empty string.

  3. Return all response headers, excluding headers that are a
    case-insensitive match for Set-Cookie or
    Set-Cookie2, as a single string, with each header line
    separated by a U+000D CR U+000A LF pair, excluding the status line, and
    with each header name and header value separated by a
    U+003A COLON U+0020 SPACE pair.

The Cross-Origin Resource Sharing specification filters
response headers exposed by
getAllResponseHeaders()
for cross-origin requests.
[CORS]

For the following script:

var client = new XMLHttpRequest();
client.open("GET", "narwhals-too.txt", true);
client.send();
client.onreadystatechange = function() {
  if(this.readyState == 2) {
    print(this.getAllResponseHeaders());
  }
}

The print() function will get to process something
like:

Date: Sun, 24 Oct 2004 04:58:38 GMT
Server: Apache/1.3.31 (Unix)
Keep-Alive: timeout=15, max=99
Connection: Keep-Alive
Transfer-Encoding: chunked
Content-Type: text/plain; charset=utf-8

4.7.5 Response entity body

The response MIME type is the
MIME type the Content-Type header contains excluding any
parameters and
converted to ASCII lowercase, or null if
the response header can not be parsed or was omitted. The
override MIME type is initially null
and can get a value if
overrideMimeType()
is invoked. Final MIME type is the
override MIME type unless that is null in which case it is
the response MIME type.

The response charset is the value of
the charset parameter of the Content-Type header
or null if there was no charset parameter or the header could
not be parsed or was omitted. The
override charset is initially null and
can get a value if overrideMimeType() is invoked.
Final charset is the
override charset unless
that is null in which case it is the response charset.


The response entity body is the
fragment of the entity body of the
response received so far
(LOADING) or the complete
entity body of the response
(DONE). If the response
does not have an entity body, the
response entity body is null.

The response entity body is updated as part
of the send() method and reset by the
open() method.


The
arraybuffer response entity body
is either an ArrayBuffer representing
the response entity body or null. If the
arraybuffer response entity body is null, let it be the return value of the
following algorithm:

  1. If the response entity body is null, return an empty
    ArrayBuffer object.

  2. Return an ArrayBuffer
    object representing the response entity body.

The
blob response entity body is either a
Blob representing the
response entity body or null. If the blob response entity body
is null, set it to the return value of the following algorithm:

  1. If the response entity body is null, return an empty
    Blob object.

  2. Return a Blob object
    representing the response entity body.

The
document response entity body
is either a document
representing the response entity body or null. If the
document response entity body is null, set it to the return value of the
following algorithm:

  1. If the response entity body is null, return null.

  2. If the
    JavaScript global environment is a
    worker environment, return null.

  3. If final MIME type is not null,
    text/html, text/xml,
    application/xml, or does not end in
    +xml, return null.

  4. If responseType is
    the empty string and final MIME type is
    text/html, return null.

    This is restricted to
    responseType being
    «document» in order to prevent breaking legacy
    content.

  5. If final MIME type is text/html, run these
    substeps:

    1. Let charset be the final charset.

    2. If charset is null,
      prescan
      the first 1024 bytes of the response entity body and if
      that does not terminate unsuccessfully then let charset be
      the return value.

    3. If charset is null, set charset to
      utf-8.

    4. Let document be a
      document that
      represents the result parsing response entity body following the rules set
      forth in the HTML Standard for an HTML parser with scripting disabled and
      a known definite encoding charset.
      [HTML]

    5. Flag document as an
      HTML document.

  6. Otherwise, let document be a
    document
    that represents the result of parsing the
    response entity body following the rules set forth in the
    XML specifications. If that fails (unsupported character encoding,
    namespace well-formedness error, etc.), return null.
    [XML] [XMLNS]

    Scripts in the resulting document tree will not be
    executed, resources referenced will not be loaded and no associated XSLT
    will be applied.

  7. If charset is null, set charset to
    utf-8.

  8. Set document‘s
    encoding to
    charset.

  9. Set document‘s
    content type
    to final MIME type.

  10. Set document‘s
    URL to
    request URL.

  11. Set document‘s
    origin to
    source origin.

  12. Return document.

The JSON response entity body is either a JavaScript value
representing the response entity body. If the
JSON response entity body is null, set it to the return value of the following
algorithm:

  1. Let JSON text be the result of running
    utf-8 decode on byte stream
    response entity body.

  2. Return the result of invoking the initial value of the parse
    property of the JSON object defined in JavaScript, with
    JSON text as its only argument, or null if that function
    throws an exception. [ECMASCRIPT]

The text response entity body is either a
string representing the response entity body or null. If the
text response entity body is null, set it to the return value of the
following algorithm:

  1. If the response entity body is null, return the empty string.

  2. Let charset be the final charset.

  3. If responseType is
    the empty string, charset is null, and
    final MIME type is either null, text/xml,
    application/xml or ends in +xml, use the
    rules set forth in the XML specifications to determine the encoding. Let
    charset be the determined encoding.
    [XML] [XMLNS]

    This is restricted to
    responseType being
    the empty string to keep the non-legacy
    responseType value
    «text» simple.

  4. If charset is null, set charset to
    utf-8.

  5. Return the result of running
    decode on byte stream
    response entity body using fallback encoding
    charset.

Authors are strongly encouraged to always encode their
resources using utf-8.

4.7.6 The overrideMimeType() method

client . overrideMimeType(mime)

Sets the Content-Type header for the response to
mime.

Throws an «InvalidStateError»
exception if the state is
LOADING or
DONE.

Throws a JavaScript TypeError if
mime is not a valid media type.

The
overrideMimeType(mime)
method must run these steps:

  1. If the state is
    LOADING or
    DONE,
    throw an
    «InvalidStateError» exception.

  2. If parsing mime analogously to the value of
    the Content-Type header fails,
    throw a JavaScript TypeError.

  3. If mime is successfully parsed, set
    override MIME type to its MIME type,
    excluding any parameters, and
    converted to ASCII lowercase.

  4. If a charset parameter is successfully parsed, set
    override charset to its value.

4.7.7 The responseType attribute

client . responseType [ = value ]

Returns the response type.

Can be set to change the response type. Values are:
the empty string (default),
«arraybuffer«,
«blob«,
«document«,
«json«, and
«text«.

When set: setting to «document» is ignored if the
JavaScript global environment is a
worker environment

When set: throws an
«InvalidStateError» exception if the
state is LOADING or
DONE.

When set: throws an
«InvalidAccessError» exception if the
synchronous flag is set and the
JavaScript global environment is a
document environment.

The
responseType
attribute must return its value. Initially its value must be the empty
string.

Setting the
responseType
attribute must run these steps:

  1. If the state is
    LOADING or
    DONE,
    throw an
    «InvalidStateError» exception.

  2. If the
    JavaScript global environment is a
    document environment and the
    synchronous flag is set,
    throw an
    «InvalidAccessError» exception.

  3. If the
    JavaScript global environment is a
    worker environment and the given
    value is «document«, terminate these steps.

  4. Set the
    responseType
    attribute’s value to the given value.

4.7.8 The response attribute

client . response

Returns the response entity body.

The
response
attribute must return the result of running these
steps:

If responseType
is the empty string or «text«
  1. If the state is not
    LOADING or
    DONE, return the empty string.

  2. If the error flag is set, return the empty string.

  3. Return the text response entity body.

Otherwise
  1. If the state is not
    DONE, return null.

  2. If the error flag is set, return null.

  3. If
    responseType is
    «arraybuffer«

    Return the
    arraybuffer response entity body.

    If
    responseType is
    «blob«

    Return the
    blob response entity body.

    If
    responseType is
    «document«

    Return the
    document response entity body.

    If
    responseType is
    «json«

    Return the
    JSON response entity body.

4.7.9 The responseText attribute

client . responseText

Returns the text response entity body.

Throws an «InvalidStateError»
exception if
responseType is not
the empty string or «text«.

The
responseText
attribute must return the result of running these
steps:

  1. If
    responseType is not
    the empty string or «text«,
    throw an
    «InvalidStateError» exception.

  2. If the state is not
    LOADING or
    DONE, return the empty string.

  3. If the error flag is set, return the empty string.

  4. Return the text response entity body.

4.7.10 The responseXML attribute

client . responseXML

Returns the document response entity body.

Throws an «InvalidStateError»
exception if
responseType is not
the empty string or «document«.

The
responseXML
attribute must return the result of running these steps:

  1. If
    responseType is not
    the empty string or «document«,
    throw an
    «InvalidStateError» exception.

  2. If the state is not
    DONE, return null.

  3. If the error flag is set, return null.

  4. Return the document response entity body.

The
responseXML attribute
has XML in its name for historical reasons. It also returns HTML resources
as documents.

4.8 Events summary

This section is non-normative.

The following events are dispatched on XMLHttpRequest
and/or XMLHttpRequestUpload objects:

Event name Interface Dispatched when…
readystatechange Event The readyState attribute changes
value, except when it changes to UNSENT.
loadstart ProgressEvent The request starts.
progress ProgressEvent Transmitting data.
abort ProgressEvent The request has been aborted. For instance, by invoking the
abort() method.
error ProgressEvent The request has failed.
load ProgressEvent The request has successfully completed.
timeout ProgressEvent The author specified timeout has passed before the request
completed.
loadend ProgressEvent The request has completed (either in success or failure).

5 Interface FormData

[Constructor(optional HTMLFormElement form)]
interface FormData {
  void append([EnsureUTF16] DOMString name, Blob value, optional [EnsureUTF16] DOMString filename);
  void append([EnsureUTF16] DOMString name, [EnsureUTF16] DOMString value);
};

If the JavaScript global environment is a
worker environment, FormData must be
exposed to JavaScript as if the constructor part of the
IDL reads [Constructor] (i.e. has no arguments).

The FormData object represents an ordered list of
entries. Each
entry consists of a
name and a
value.

For the purposes of interaction with other algorithms, an
entry’s type is «string» if
value is a string and «file» otherwise. If
an entry’s type is «file», its filename is the
value of entry’s
value’s
name attribute.

fd = new FormData([form])

Returns a new FormData object, optionally initialized with the
entries from form (if given).

fd . append(name, value [, filename])

Appends a new entry to the
FormData object.

The
FormData(form)
constructor must run these steps:

  1. Let fd be a new FormData object.

  2. If form is given, set fd‘s
    entries to the result of
    constructing the form data set for form.

  3. Return fd.

The
append(name, value, filename)
method must run these steps:

  1. Let entry be a new
    entry.

  2. Set entry‘s name
    to name.

  3. If value is a Blob, set
    value to a new File object whose
    name attribute value is
    «blob«.

  4. If value is a File and
    filename is given, set value‘s
    name attribute value to
    filename.

  5. Set entry‘s value
    to value.

  6. Append entry to FormData object’s list of
    entries.

References

[COOKIES]
HTTP State Management Mechanism, Adam Barth. IETF.

[CORS]
Cross-Origin Resource Sharing, Anne van Kesteren. W3C.

[DOM]
DOM, Anne van Kesteren, Aryeh Gregor, Ms2ger et al.. W3C.

[DOMPS]
DOM Parsing and Serialization, Travis Leithead and Ms2ger. W3C.

[ECMASCRIPT]
ECMAScript Language Specification. ECMA.

[ENCODING]
Encoding Standard, Anne van Kesteren. WHATWG.

[FILEAPI]
File API, Arun Ranganathan and Jonas Sicking. W3C.

[HTML]
HTML, Robin Berjon, Travis Leithead, Erika Doyle Navara et al.. W3C.

[HTTP]
Hypertext Transfer Protocol — HTTP/1.1, Roy Fielding, James Gettys, Jeffrey Mogul et al.. IETF.

[HTTPAUTH]
HTTP Authentication: Basic and Digest Access Authentication, J. Franks, Phillip Hallam-Baker, J. Hostetler et al.. IETF.

[HTTPVERBSEC]
Multiple vendors’ web servers enable HTTP TRACE method by default. US-CERT.

Microsoft Internet Information Server (IIS) vulnerable to cross-site scripting via HTTP TRACK method. US-CERT.

HTTP proxy default configurations allow arbitrary TCP connections. US-CERT.

[PROGRESSEVENTS]
Progress Events, Anne van Kesteren, Charles McCathieNevile and Jungkee Song. W3C.

[RFC2119]
Key words for use in RFCs to Indicate Requirement Levels, Scott Bradner. IETF.

[TYPEDARRAY]
Typed Array, David Herman and Kenneth Russell. Khronos.

[URL]
URL Standard, Anne van Kesteren. WHATWG.

[WEBIDL]
Web IDL, Cameron McCormack. W3C.

[XML]
Extensible Markup Language, Tim Bray, Jean Paoli, C. M. Sperberg-McQueen et al.. W3C.

[XMLNS]
Namespaces in XML, Tim Bray, Dave Hollander, Andrew Layman et al.. W3C.

Acknowledgments

The editor would like to thank

Addison Phillips,
Adrian Bateman,
Ahmed Kamel,
Alex Hopmann,
Alex Vincent,
Alexey Proskuryakov,
Andrea Marchesini,
Asbjørn Ulsberg,
Boris Zbarsky,
Björn Höhrmann,
Cameron McCormack,
Chris Marrin,
Christophe Jolif,
Charles McCathieNevile,
Dan Winship,
David Andersson,
David Flanagan,
David Håsäther,
David Levin,
Dean Jackson,
Denis Sureau,
Dominik Röttsches,
Doug Schepers,
Douglas Livingstone,
Elliott Sprehn,
Elliotte Harold,
Eric Lawrence,
Eric Uhrhane,
Erik Arvidsson
Erik Dahlström,
Feras Moussa,
Sam Sneddon,
Gideon Cohn,
Glenn Adams,
Gorm Haug Eriksen,
Håkon Wium Lie,
Hallvord R. M. Steen,
Henri Sivonen,
Huub Schaeks,
Ian Davis,
Ian Hickson,
Ivan Herman,
Jarred Nicholls,
Jeff Walden,
Jens Lindström,
Jim Deegan,
Jim Ley,
Joe Farro,
Jonas Sicking,
Julian Reschke,
송정기 (Jungkee Song),
呂康豪 (Kang-Hao Lu),
Karl Dubost,
Lachlan Hunt,
Maciej Stachowiak,
Magnus Kristiansen,
Marc Hadley,
Marcos Caceres,
Mark Baker,
Mark Birbeck,
Mark Nottingham,
Mark S. Miller,
Martin Hassman,
Mohamed Zergaoui,
Ms2ger,
Odin Hørthe Omdal,
Olli Pettay,
Pawel Glowacki,
Peter Michaux,
Philip Taylor,
Robin Berjon,
Rune F. Halvorsen,
Ruud Steltenpool,
Sergiu Dumitriu,
Sigbjørn Finne,
Simon Pieters,
Stewart Brodie,
Sunava Dutta,
Takeshi Yoshino,
Thomas Roessler,
Tom Magliery,
Travis Leithead
Yehuda Katz, and
Zhenbin Xu

for their contributions to this specification.

Special thanks to the Microsoft employees who first implemented the
XMLHttpRequest interface, which was first widely
deployed by the Windows Internet Explorer browser.

Special thanks also to the WHATWG for drafting an initial version of
this specification in their Web Applications 1.0 document (now renamed to
HTML). [HTML]

Special thanks to Anne van Kesteren who has provided nearly all the contents until he stepped down as a W3C editor and is now in succession providing discussions and contents as the editor of the XMLHttpRequest Living Standard in WHATWG which this version of the specification pursues convergence.

Thanks also to all those who have helped to improve this specification
by sending suggestions and corrections. (Please, keep bugging us with your
issues!)

XMLHttpRequest

XMLHttpRequest — это встроенный в браузер объект, который даёт возможность делать HTTP-запросы к серверу без перезагрузки страницы.

Несмотря на наличие слова «XML» в названии, XMLHttpRequest может работать с любыми данными, а не только с XML. Мы можем загружать/скачивать файлы, отслеживать прогресс и многое другое.

На сегодняшний день не обязательно использовать XMLHttpRequest, так как существует другой, более современный метод fetch.

В современной веб-разработке XMLHttpRequest используется по трём причинам:

  1. По историческим причинам: существует много кода, использующего XMLHttpRequest, который нужно поддерживать.
  2. Необходимость поддерживать старые браузеры и нежелание использовать полифилы (например, чтобы уменьшить количество кода).
  3. Потребность в функциональности, которую fetch пока что не может предоставить, к примеру, отслеживание прогресса отправки на сервер.

Что-то из этого списка звучит знакомо? Если да, тогда вперёд, приятного знакомства с XMLHttpRequest. Если же нет, возможно, имеет смысл изучать сразу info:fetch.

Основы

XMLHttpRequest имеет два режима работы: синхронный и асинхронный.

Сначала рассмотрим асинхронный, так как в большинстве случаев используется именно он.

Чтобы сделать запрос, нам нужно выполнить три шага:

  1. Создать XMLHttpRequest.

    let xhr = new XMLHttpRequest(); // у конструктора нет аргументов

    Конструктор не имеет аргументов.

  2. Инициализировать его.

    xhr.open(method, URL, [async, user, password])

    Этот метод обычно вызывается сразу после new XMLHttpRequest. В него передаются основные параметры запроса:

    • method — HTTP-метод. Обычно это "GET" или "POST".
    • URL — URL, куда отправляется запрос: строка, может быть и объект URL.
    • async — если указать false, тогда запрос будет выполнен синхронно, это мы рассмотрим чуть позже.
    • user, password — логин и пароль для базовой HTTP-авторизации (если требуется).

    Заметим, что вызов open, вопреки своему названию, не открывает соединение. Он лишь конфигурирует запрос, но непосредственно отсылается запрос только лишь после вызова send.

  3. Послать запрос.

    Этот метод устанавливает соединение и отсылает запрос к серверу. Необязательный параметр body содержит тело запроса.

    Некоторые типы запросов, такие как GET, не имеют тела. А некоторые, как, например, POST, используют body, чтобы отправлять данные на сервер. Мы позже увидим примеры.

  4. Слушать события на xhr, чтобы получить ответ.

    Три наиболее используемых события:

    • load — происходит, когда получен какой-либо ответ, включая ответы с HTTP-ошибкой, например 404.
    • error — когда запрос не может быть выполнен, например, нет соединения или невалидный URL.
    • progress — происходит периодически во время загрузки ответа, сообщает о прогрессе.
    xhr.onload = function() {
      alert(`Загружено: ${xhr.status} ${xhr.response}`);
    };
    
    xhr.onerror = function() { // происходит, только когда запрос совсем не получилось выполнить
      alert(`Ошибка соединения`);
    };
    
    xhr.onprogress = function(event) { // запускается периодически
      // event.loaded - количество загруженных байт
      // event.lengthComputable = равно true, если сервер присылает заголовок Content-Length
      // event.total - количество байт всего (только если lengthComputable равно true)
      alert(`Загружено ${event.loaded} из ${event.total}`);
    };

Вот полный пример. Код ниже загружает /article/xmlhttprequest/example/load с сервера и сообщает о прогрессе:

// 1. Создаём новый XMLHttpRequest-объект
let xhr = new XMLHttpRequest();

// 2. Настраиваем его: GET-запрос по URL /article/.../load
xhr.open('GET', '/article/xmlhttprequest/example/load');

// 3. Отсылаем запрос
xhr.send();

// 4. Этот код сработает после того, как мы получим ответ сервера
xhr.onload = function() {
  if (xhr.status != 200) { // анализируем HTTP-статус ответа, если статус не 200, то произошла ошибка
    alert(`Ошибка ${xhr.status}: ${xhr.statusText}`); // Например, 404: Not Found
  } else { // если всё прошло гладко, выводим результат
    alert(`Готово, получили ${xhr.response.length} байт`); // response -- это ответ сервера
  }
};

xhr.onprogress = function(event) {
  if (event.lengthComputable) {
    alert(`Получено ${event.loaded} из ${event.total} байт`);
  } else {
    alert(`Получено ${event.loaded} байт`); // если в ответе нет заголовка Content-Length
  }

};

xhr.onerror = function() {
  alert("Запрос не удался");
};

После ответа сервера мы можем получить результат запроса в следующих свойствах xhr:

status
: Код состояния HTTP (число): 200, 404, 403 и так далее, может быть 0 в случае, если ошибка не связана с HTTP.

statusText
: Сообщение о состоянии ответа HTTP (строка): обычно OK для 200, Not Found для 404, Forbidden для 403, и так далее.

response (в старом коде может встречаться как responseText)
: Тело ответа сервера.

Мы можем также указать таймаут — промежуток времени, который мы готовы ждать ответ:

xhr.timeout = 10000; // таймаут указывается в миллисекундах, т.е. 10 секунд

Если запрос не успевает выполниться в установленное время, то он прерывается, и происходит событие timeout.

Чтобы добавить к URL параметры, вида `?name=value`, и корректно закодировать их, можно использовать объект [URL](info:url):

```js
let url = new URL('https://google.com/search');
url.searchParams.set('q', 'test me!');

// параметр 'q' закодирован
xhr.open('GET', url); // https://google.com/search?q=test+me%21
```

Тип ответа

Мы можем использовать свойство xhr.responseType, чтобы указать ожидаемый тип ответа:

  • "" (по умолчанию) — строка,
  • "text" — строка,
  • "arraybuffer"ArrayBuffer (для бинарных данных, смотрите в info:arraybuffer-binary-arrays),
  • "blob"Blob (для бинарных данных, смотрите в info:blob),
  • "document" — XML-документ (может использовать XPath и другие XML-методы),
  • "json" — JSON (парсится автоматически).

К примеру, давайте получим ответ в формате JSON:

let xhr = new XMLHttpRequest();

xhr.open('GET', '/article/xmlhttprequest/example/json');

*!*
xhr.responseType = 'json';
*/!*

xhr.send();

// тело ответа {"сообщение": "Привет, мир!"}
xhr.onload = function() {
  let responseObj = xhr.response;
  alert(responseObj.message); // Привет, мир!
};
В старом коде вы можете встретить свойства `xhr.responseText` и даже `xhr.responseXML`.

Они существуют по историческим причинам, раньше с их помощью получали строки или XML-документы. Сегодня следует устанавливать желаемый тип объекта в `xhr.responseType` и получать `xhr.response`, как показано выше.

Состояния запроса

У XMLHttpRequest есть состояния, которые меняются по мере выполнения запроса. Текущее состояние можно посмотреть в свойстве xhr.readyState.

Список всех состояний, указанных в спецификации:

UNSENT = 0; // исходное состояние
OPENED = 1; // вызван метод open
HEADERS_RECEIVED = 2; // получены заголовки ответа
LOADING = 3; // ответ в процессе передачи (данные частично получены)
DONE = 4; // запрос завершён

Состояния объекта XMLHttpRequest меняются в таком порядке: 0 -> 1 -> 2 -> 3 -> … -> 3 -> 4. Состояние 3 повторяется каждый раз, когда получена часть данных.

Изменения в состоянии объекта запроса генерируют событие readystatechange:

xhr.onreadystatechange = function() {
  if (xhr.readyState == 3) {
    // загрузка
  }
  if (xhr.readyState == 4) {
    // запрос завершён
  }
};

Вы можете наткнуться на обработчики события readystatechange в очень старом коде, так уж сложилось исторически, когда-то не было событий load и других. Сегодня из-за существования событий load/error/progress можно сказать, что событие readystatechange «морально устарело».

Отмена запроса

Если мы передумали делать запрос, можно отменить его вызовом xhr.abort():

xhr.abort(); // завершить запрос

При этом генерируется событие abort, а xhr.status устанавливается в 0.

Синхронные запросы

Если в методе open третий параметр async установлен на false, запрос выполняется синхронно.

Другими словами, выполнение JavaScript останавливается на send() и возобновляется после получения ответа. Так ведут себя, например, функции alert или prompt.

Вот переписанный пример с параметром async, равным false:

let xhr = new XMLHttpRequest();

xhr.open('GET', '/article/xmlhttprequest/hello.txt', *!*false*/!*);

try {
  xhr.send();
  if (xhr.status != 200) {
    alert(`Ошибка ${xhr.status}: ${xhr.statusText}`);
  } else {
    alert(xhr.response);
  }
} catch(err) { // для отлова ошибок используем конструкцию try...catch вместо onerror
  alert("Запрос не удался");
}

Выглядит, может быть, и неплохо, но синхронные запросы используются редко, так как они блокируют выполнение JavaScript до тех пор, пока загрузка не завершена. В некоторых браузерах нельзя прокручивать страницу, пока идёт синхронный запрос. Ну а если же синхронный запрос по какой-то причине выполняется слишком долго, браузер предложит закрыть «зависшую» страницу.

Многие продвинутые возможности XMLHttpRequest, такие как выполнение запроса на другой домен или установка таймаута, недоступны для синхронных запросов. Также, как вы могли заметить, ни о какой индикации прогресса речь тут не идёт.

Из-за всего этого синхронные запросы используют очень редко. Мы более не будем рассматривать их.

HTTP-заголовки

XMLHttpRequest умеет как указывать свои заголовки в запросе, так и читать присланные в ответ.

Для работы с HTTP-заголовками есть 3 метода:

setRequestHeader(name, value)
: Устанавливает заголовок запроса с именем name и значением value.

Например:

```js
xhr.setRequestHeader('Content-Type', 'application/json');
```

```warn header="Ограничения на заголовки"
Некоторые заголовки управляются исключительно браузером, например `Referer` или `Host`, а также ряд других.
Полный список [тут](https://www.w3.org/TR/XMLHttpRequest/#the-setrequestheader-method).

`XMLHttpRequest` не разрешено изменять их ради безопасности пользователей и для обеспечения корректности HTTP-запроса.
```

````warn header="Поставленный заголовок нельзя снять"
Ещё одной особенностью `XMLHttpRequest` является то, что отменить `setRequestHeader` невозможно.

Если заголовок определён, то его нельзя снять. Повторные вызовы лишь добавляют информацию к заголовку, а не перезаписывают его.

Например:

```js
xhr.setRequestHeader('X-Auth', '123');
xhr.setRequestHeader('X-Auth', '456');

// заголовок получится такой:
// X-Auth: 123, 456
```
````

getResponseHeader(name)
: Возвращает значение заголовка ответа name (кроме Set-Cookie и Set-Cookie2).

Например:

```js
xhr.getResponseHeader('Content-Type')
```

getAllResponseHeaders()
: Возвращает все заголовки ответа, кроме Set-Cookie и Set-Cookie2.

Заголовки возвращаются в виде единой строки, например:

```http
Cache-Control: max-age=31536000
Content-Length: 4260
Content-Type: image/png
Date: Sat, 08 Sep 2012 16:53:16 GMT
```

Между заголовками всегда стоит перевод строки в два символа `"rn"` (независимо от ОС), так что мы можем легко разделить их на отдельные заголовки. Значение заголовка всегда отделено двоеточием с пробелом `": "`. Этот формат задан стандартом.

Таким образом, если хочется получить объект с парами заголовок-значение, нам нужно задействовать немного JS.

Вот так (предполагается, что если два заголовка имеют одинаковое имя, то последний перезаписывает предыдущий):

```js
let headers = xhr
  .getAllResponseHeaders()
  .split('rn')
  .reduce((result, current) => {
    let [name, value] = current.split(': ');
    result[name] = value;
    return result;
  }, {});

// headers['Content-Type'] = 'image/png'
```

POST, FormData

Чтобы сделать POST-запрос, мы можем использовать встроенный объект FormData.

Синтаксис:

let formData = new FormData([form]); // создаём объект, по желанию берём данные формы <form>
formData.append(name, value); // добавляем поле

Мы создаём объект, при желании указываем, из какой формы form взять данные, затем, если нужно, с помощью метода append добавляем дополнительные поля, после чего:

  1. xhr.open('POST', ...) – создаём POST-запрос.
  2. xhr.send(formData) – отсылаем форму серверу.

Например:

<form name="person">
  <input name="name" value="Петя">
  <input name="surname" value="Васечкин">
</form>

<script>
  // заполним FormData данными из формы
  let formData = new FormData(document.forms.person);

  // добавим ещё одно поле
  formData.append("middle", "Иванович");

  // отправим данные
  let xhr = new XMLHttpRequest();
  xhr.open("POST", "/article/xmlhttprequest/post/user");
  xhr.send(formData);

  xhr.onload = () => alert(xhr.response);
</script>

Обычно форма отсылается в кодировке multipart/form-data.

Если нам больше нравится формат JSON, то используем JSON.stringify и отправляем данные как строку.

Важно не забыть поставить соответствующий заголовок Content-Type: application/json, многие серверные фреймворки автоматически декодируют JSON при его наличии:

let xhr = new XMLHttpRequest();

let json = JSON.stringify({
  name: "Вася",
  surname: "Петров"
});

xhr.open("POST", '/submit')
xhr.setRequestHeader('Content-type', 'application/json; charset=utf-8');

xhr.send(json);

Метод .send(body) весьма всеяден. Он может отправить практически что угодно в body, включая объекты типа Blob и BufferSource.

Прогресс отправки

Событие progress срабатывает только на стадии загрузки ответа с сервера.

А именно: если мы отправляем что-то через POST-запрос, XMLHttpRequest сперва отправит наши данные (тело запроса) на сервер, а потом загрузит ответ сервера. И событие progress будет срабатывать только во время загрузки ответа.

Если мы отправляем что-то большое, то нас гораздо больше интересует прогресс отправки данных на сервер. Но xhr.onprogress тут не поможет.

Существует другой объект, без методов, только для отслеживания событий отправки: xhr.upload.

Он генерирует события, похожие на события xhr, но только во время отправки данных на сервер:

  • loadstart — начало загрузки данных.
  • progress — генерируется периодически во время отправки на сервер.
  • abort — загрузка прервана.
  • error — ошибка, не связанная с HTTP.
  • load — загрузка успешно завершена.
  • timeout — вышло время, отведённое на загрузку (при установленном свойстве timeout).
  • loadend — загрузка завершена, вне зависимости от того, как — успешно или нет.

Примеры обработчиков для этих событий:

xhr.upload.onprogress = function(event) {
  alert(`Отправлено ${event.loaded} из ${event.total} байт`);
};

xhr.upload.onload = function() {
  alert(`Данные успешно отправлены.`);
};

xhr.upload.onerror = function() {
  alert(`Произошла ошибка во время отправки: ${xhr.status}`);
};

Пример из реальной жизни: загрузка файла на сервер с индикацией прогресса:

<input type="file" onchange="upload(this.files[0])">

<script>
function upload(file) {
  let xhr = new XMLHttpRequest();

  // отслеживаем процесс отправки
*!*
  xhr.upload.onprogress = function(event) {
    console.log(`Отправлено ${event.loaded} из ${event.total}`);
  };
*/!*

  // Ждём завершения: неважно, успешного или нет
  xhr.onloadend = function() {
    if (xhr.status == 200) {
      console.log("Успех");
    } else {
      console.log("Ошибка " + this.status);
    }
  };

  xhr.open("POST", "/article/xmlhttprequest/post/upload");
  xhr.send(file);
}
</script>

Запросы на другой источник

XMLHttpRequest может осуществлять запросы на другие сайты, используя ту же политику CORS, что и fetch.

Точно так же, как и при работе с fetch, по умолчанию на другой источник не отсылаются куки и заголовки HTTP-авторизации. Чтобы это изменить, установите xhr.withCredentials в true:

let xhr = new XMLHttpRequest();
*!*
xhr.withCredentials = true;
*/!*

xhr.open('POST', 'http://anywhere.com/request');
...

Детали по заголовкам, которые при этом необходимы, смотрите в главе fetch.

Итого

Типичный код GET-запроса с использованием XMLHttpRequest:

let xhr = new XMLHttpRequest();

xhr.open('GET', '/my/url');

xhr.send();

xhr.onload = function() {
  if (xhr.status != 200) { // HTTP ошибка?
    // обработаем ошибку
    alert( 'Ошибка: ' + xhr.status);
    return;
  }

  // получим ответ из xhr.response
};

xhr.onprogress = function(event) {
  // выведем прогресс
  alert(`Загружено ${event.loaded} из ${event.total}`);
};

xhr.onerror = function() {
  // обработаем ошибку, не связанную с HTTP (например, нет соединения)
};

Событий на самом деле больше, в современной спецификации они все перечислены в том порядке, в каком генерируются во время запроса:

  • loadstart — начало запроса.
  • progress — прибыла часть данных ответа, тело ответа полностью на данный момент можно получить из свойства responseText.
  • abort — запрос был прерван вызовом xhr.abort().
  • error — произошла ошибка соединения, например неправильное доменное имя. Событие не генерируется для HTTP-ошибок как, например, 404.
  • load — запрос успешно завершён.
  • timeout — запрос был отменён по причине истечения отведённого для него времени (происходит, только если был установлен таймаут).
  • loadend — срабатывает после load, error, timeout или abort.

События error, abort, timeout и load взаимно исключают друг друга — может произойти только одно из них.

Наиболее часто используют события завершения загрузки (load), ошибки загрузки (error), или мы можем использовать единый обработчик loadend для всего и смотреть в свойствах объекта запроса xhr детали произошедшего.

Также мы уже видели событие: readystatechange. Исторически оно появилось одним из первых, даже раньше, чем была составлена спецификация. Сегодня нет необходимости использовать его, так как оно может быть заменено современными событиями, но на него можно часто наткнуться в старом коде.

Если же нам нужно следить именно за процессом отправки данных на сервер, тогда можно использовать те же события, но для объекта xhr.upload.

XMLHttpRequest is a built-in browser object that allows to make HTTP requests in JavaScript.

Despite of having the word «XML» in its name, it can operate on any data, not only in XML format.

Asynchronous XMLHttpRequest

XMLHttpRequest has two modes of operation: synchronous and asynchronous.

First let’s see the asynchronous variant as it’s used in the majority of cases.

The code below loads the URL at /article/xmlhttprequest/hello.txt from the server and shows its content on-screen:

// 1. Create a new XMLHttpRequest object
let xhr = new XMLHttpRequest();

// 2. Configure it: GET-request for the URL /article/.../hello.txt
xhr.open('GET', '/article/xmlhttprequest/hello.txt');

// 3. Send the request over the network
xhr.send();

// 4. This will be called after the response is received
xhr.onload = function() {
  if (xhr.status != 200) { // analyze HTTP status of the response
    // if it's not 200, consider it an error
    alert(xhr.status + ': ' + xhr.statusText); // e.g. 404: Not Found
  } else {
    // show the result
    alert(xhr.responseText); // responseText is the server response
  }
};

As we can see, there are several methods of XMLHttpRequest here. Let’s cover them.

Setup: «open»

The syntax:

xhr.open(method, URL, async, user, password)

This method is usually called first after new XMLHttpRequest. It specifies the main parameters of the request:

  • method — HTTP-method. Usually "GET" or "POST", but we can also use TRACE/DELETE/PUT and so on.
  • URL — the URL to request. Can use any path and protocol, but there are cross-domain limitations called «Same Origin Policy». We can make any requests to the same protocol://domain:port that the current page comes from, but other locations are «forbidden» by default (unless they implement special HTTP-headers, we’ll cover them in chapter [todo]).
  • async — if the third parameter is explicitly set to false, then the request is synchronous, otherwise it’s asynchronous. We’ll talk more about that in this chapter soon.
  • user, password — login and password for basic HTTP auth (if required).

Please note that open call, contrary to its name, does not open the connection. It only configures the request, but the network activity only starts with the call of send.

Send it out: «send»

The syntax:

xhr.send([body])

This method opens the connection and sends the request to server. The optional body parameter contains the request body. Some request methods like GET do not have a body. And some of them like POST use body to send the data. We’ll see examples with a body in the next chapter.

Cancel: abort and timeout

If we changed our mind, we can terminate the request at any time. The call to xhr.abort() does that:

xhr.abort(); // terminate the request

We can also specify a timeout using the corresponding property:

xhr.timeout = 10000;

The timeout is expressed in ms. If the request does not succeed within the given time, it gets canceled automatically.

Events: onload, onerror etc

A request is asynchronous by default. In other words, the browser sends it out and allows other JavaScript code to execute.

After the request is sent, xhr starts to generate events. We can use addEventListener or on<event> properties to handle them, just like with DOM objects.

The modern specification lists following events:

  • loadstart — the request has started.
  • progress — the browser received a data packet (can happen multiple times).
  • abort — the request was aborted by xhr.abort().
  • error — an network error has occurred, the request failed.
  • load — the request is successful, no errors.
  • timeout — the request was canceled due to timeout (if the timeout is set).
  • loadend — the request is done (with an error or without it)
  • readystatechange — the request state is changed (will cover later).

Using these events we can track successful loading (onload), errors (onerror) and the amount of the data loaded (onprogress).

Please note that errors here are «communication errors». In other words, if the connection is lost or the remote server does not respond at all — then it’s the error in the terms of XMLHttpRequest. Bad HTTP status like 500 or 404 are not considered errors.

Here’s a more feature-full example, with errors and a timeout:

<script>
  function load(url) {
    let xhr = new XMLHttpRequest();
    xhr.open('GET', url);
    xhr.timeout = 1000;
    xhr.send();

    xhr.onload = function() {
      alert(`Loaded: ${this.status} ${this.responseText}`);
    };

    xhr.onerror = () => alert('Error');

    xhr.ontimeout = () => alert('Timeout!');
  }
</script>

<button onclick="load('/article/xmlhttprequest/hello.txt')">Load</button>
<button onclick="load('/article/xmlhttprequest/hello.txt?speed=0')">Load with timeout</button>
<button onclick="load('no-such-page')">Load 404</button>
<button onclick="load('http://example.com')">Load another domain</button>
  1. The first button triggers only onload as it loads the file hello.txt normally.
  2. The second button loads a very slow URL, so it calls only ontimeout (because xhr.timeout is set).
  3. The third button loads a non-existant URL, but it also calls onload (with «Loaded: 404»), because there’s no network error.
  4. The last button tries to load a page from another domain. That’s prohibited unless the remote server explicitly agrees by sending certain headers (to be covered later), so we have onerror here. The onerror handler would also trigger in other cases if we start a request, and then sever the network connection of our device.

Response: status, responseText and others

Once the server has responded, we can receive the result in the following properties of the request object:

status : HTTP status code: 200, 404, 403 and so on. Also can be 0 if an error occurred.

statusText : HTTP status message: usually OK for 200, Not Found for 404, Forbidden for 403 and so on.

responseText : The text of the server response,

If the server returns XML with the correct header Content-type: text/xml, then there’s also responseXML property with the parsed XML document. You can query it with xhr.responseXml.querySelector("...") and perform other XML-specific operations.

That’s rarely used, because most of the time JSON is returned by the server. And then we can parse it using JSON.parse(xhr.responseText).

Synchronous and asynchronous requests

If in the open method the third parameter async is set to false, the request is made synchronously.

In other words, Javascript execution pauses at that line and continues when the response is received. Somewhat like alert or prompt commands.

Synchronous calls are used rarely, because they block in-page Javascript till the loading is complete. In some browsers, a user is unable to scroll the page.

// Synchronous request
xhr.open('GET', 'phones.json', *!*false*/!*);

// Send it
xhr.send();
// ...JavaScript "hangs" and waits till the request is complete

If a synchronous call takes too much time, the browser may suggest to close the «hanging» webpage.

Also, because of the blocking, it becomes impossible to send two requests simultaneously. And, looking a bit forward, let’s note that some advanced capabilities of XMLHttpRequest, like requesting from another domain or specifying a timeout, are unavailable for synchronous requests.

Because of all that, synchronous requests are used very sparingly, almost never.

By default, requests are asynchronous.

The same request made asynchronously:

let xhr = new XMLHttpRequest();

xhr.open('GET', 'phones.json'); // the third parameter is true by default

xhr.send(); // (1)

xhr.onreadystatechange = function() { // (3)
  if (xhr.readyState != 4) return;

  button.innerHTML = 'Complete!';

  if (xhr.status != 200) {
    alert(xhr.status + ': ' + xhr.statusText);
  } else {
    alert(xhr.responseText);
  }

}

button.innerHTML = 'Loading...'; // (2)
button.disabled = true;

Now as there’s no third argument in open (or if we explicitly set it to true), the request is asynchronous. In other words, after the call xhr.send() in the line (1), Javascript does not «hang», but continues to execute.

In our case, it means that (2) shows a «loading» message.

Then, after time, when the result is received, it comes in the event handler (3) that we’ll cover a bit later.

Event «readystatechange»

The event readystatechange occurs multiple times during sending the request and receiving the response.

As the name suggests, there’s a «ready state» of XMLHttpRequest. It is accessible as xhr.readyState.

In the example above we only used state 4 (request complete), but there are few more.

All states, as in the specification:

const unsigned short UNSENT = 0; // initial state
const unsigned short OPENED = 1; // open called
const unsigned short HEADERS_RECEIVED = 2; // response headers received
const unsigned short LOADING = 3; // response is loading (a data packed is received)
const unsigned short DONE = 4; // request complete

An XMLHttpRequest object travels them in the order 0 -> 1 -> 2 -> 3 -> … -> 3 -> 4. State 3 repeats every time a data packet is received over the network.

The example above demonstrates these states. The server answers the request digits by sending a string of 1000 digits once per second.

One might think that readyState=3 (the next data packet is received) allows us to get the current (not full yet) response body in responseText.

That’s true. But only partially.

Technically, we do not have control over breakpoints between network packets. Many languages use multi-byte encodings like UTF-8, where a character is represented by multiple bytes. Some characters use only 1 byte, some use 2 or more. And packets may split in the middle of a character.

E.g. the letter `ö` is encoded with two bytes. The first of them may be at the end of one packet, and the second one — at the beginning of the next packet.

So, during the readyState, at the end of responseText there will be a half-character byte. That may lead to problems. In some simple cases, when we use only latin characters and digits (they all are encoded with 1 byte), such thing can’t happen, but in other cases, that can become a source of bugs.

HTTP-headers

XMLHttpRequest allows both to send custom headers and read headers from the response.

There are 3 methods for HTTP-headers:

setRequestHeader(name, value) : Sets the request header with the given name and value.

For instance:

xhr.setRequestHeader('Content-Type', 'application/json');

Headers limitations

Several headers are managed exclusively by the browser, e.g. Referer and Host.
The full list is in the specification .

XMLHttpRequest is not allowed to change them, for the sake of user safety and correctness of the request.

Can’t remove a header

Another peciliarity of XMLHttpRequest is that one can’t undo setRequestHeader.

Once the header is set, it’s set. Additional calls add information to the header, don’t overwrite it.

For instance:

xhr.setRequestHeader('X-Auth', '123');
xhr.setRequestHeader('X-Auth', '456');

// the header will be:
// X-Auth: 123, 456

getResponseHeader(name) : Gets the response header with the given name (except Set-Cookie and Set-Cookie2).

For instance:

xhr.getResponseHeader('Content-Type')

getAllResponseHeaders() : Returns all response headers, except Set-Cookie and Set-Cookie2.

Headers are returned as a single line, e.g.:

Cache-Control: max-age=31536000
Content-Length: 4260
Content-Type: image/png
Date: Sat, 08 Sep 2012 16:53:16 GMT

The line break between headers is always "rn" (doesn’t depend on OS), so we can easily split it into individual headers. The separator between the name and the value is always a colon followed by a space ": ". That’s fixed in the specification.

So, if we want to get an object with name/value pairs, we need to throw in a bit JS.

Like this (assuming that if two headers have the same name, then the latter one overwrites the former one):

let headers = xhr
  .getAllResponseHeaders()
  .split('rn')
  .reduce((result, current) => {
    let [name, value] = current.split(': ');
    result[name] = value;
    return acc;
  }, {});

Timeout

The maximum duration of an asynchronous request can be set using the timeout property:

xhr.timeout = 30000; // 30 seconds (in milliseconds)

If the request exceeds that time, it’s aborted, and the timeout event is generated:

xhr.ontimeout = function() {
  alert( 'Sorry, the request took too long.' );
}

The full event list

The modern specification lists following events (in the lifecycle order):

  • loadstart — the request has started.
  • progress — a data packet of the response has arrived, the whole response body at the moment is in responseText.
  • abort — the request was canceled by the call xhr.abort().
  • error — connection error has occured, e.g. wrong domain name. Doesn’t happen for HTTP-errors like 404.
  • load — the request has finished successfully.
  • timeout — the request was canceled due to timeout (only happens if it was set).
  • loadend — the request has finished (succeffully or not).

The most used events are load completion (onload), load failure (onerror), and also onprogress to track the progress.

We’ve already seen another event: readystatechange. Historically, it appeared long ago, before the specification settled. Nowadays, there’s no need to use it, we can replace it with newer events, but it can often be found in older scripts.

Summary

Typical code of the GET-request with XMLHttpRequest:

let xhr = new XMLHttpRequest();

xhr.open('GET', '/my/url');

xhr.send();

xhr.onload = function() {
  // we can check
  // status, statusText - for response HTTP status
  // responseText, responseXML (when content-type: text/xml) - for the response

  if (this.status != 200) {
    // handle error
    alert( 'error: ' + this.status);
    return;
  }

  // get the response from this.responseText
};

xhr.onerror = function() {
  // handle error
};

XMLHttpRequest is widely used, but there’s a more modern method named fetch(url) that returns a promise, thus working well with async/await. We’ll cover it soon in the next sections.

XMLHttpRequest is a built-in browser object in all modern browsers that can be used to make HTTP requests in JavaScript to exchange data between the web browser and the server.

Despite the word «XML» in its name, XMLHttpRequest can be used to retrieve any kind of data and not just XML. We can use it to upload/download files, submit form data, track progress, and much more.

Basic XHR Request

To send an HTTP request using XHR, create an XMLHttpRequest object, open a connection to the URL, and send the request. Once the request completes, the object will contain information such as the response body and the HTTP status code.

Let’s use JSONPlaceholder to test REST API to send a GET request using XHR:

// create an XHR object
const xhr = new XMLHttpRequest()

// listen for `onload` event
xhr.onload = () => {
  // process response
  if (xhr.status == 200) {
    // parse JSON data
    console.log(JSON.parse(xhr.response))
  } else {
    console.error('Error!')
  }
}

// create a `GET` request
xhr.open('GET', 'https://jsonplaceholder.typicode.com/users')

// send request
xhr.send()

The xhr.onload event only works in modern browsers (IE10+, Firefox, Chrome, Safari). If you want to support old browsers, use the xhr.onreadystatechange event instead.

xhr.open() Method

In the example above, we passed the HTTP method and a URL to the request to the open() method. This method is normally called right after new XMLHttpRequest(). We can use this method to specify the main parameters of the request:

Here is the syntax of this method:

xhr.open(method, URL, [async, user, password])
  • method — HTTP request method. It can be GET, POST, DELETE, PUT, etc.
  • URL — The URL to request, a string or a URL object
  • asnyc — Specify whether the request should be made asynchronously or not. The default value is true
  • username & password — Credentials for basic HTTP authentication

The open() method does not open the connection to the URL. It only configures the HTTP request.

xhr.send() Method

xhr.send([body])

The send() method opens the network connection and sends the request to the server. It takes an optional body parameter that contains the request body. For request methods like GET you do not need to pass the body parameter.

XHR Events

The three most widely used XHR events are the following:

  • load — This event is invoked when the result is ready. It is equivalent to the xhr.onreadystatechange event with xhr.readyState == 4.
  • error — This event is fired when the request is failed due to a network down or invalid URL.
  • progress — This event is triggered periodically during the response download. It can be used to report progress for large network requests.
// listen for `load` event
xhr.onload = () => {
  console.log(`Data Loaded: ${xhr.status} ${xhr.response}`)
}

// listen for `error` event
xhr.onerror = () => {
  console.error('Request failed.')
}

// listen for `progress` event
xhr.onprogress = event => {
  // event.loaded returns how many bytes are downloaded
  // event.total returns the total number of bytes
  // event.total is only available if server sends `Content-Length` header
  console.log(`Downloaded ${event.loaded} of ${event.total}`)
}

Request Timeout

You can easily configure the request timeout by specifying the time in milliseconds:

// set timeout
xhr.timeout = 5000 // 5 seconds

// listen for `timeout` event
xhr.ontimeout = () => console.log('Request timeout.', xhr.responseURL)

xhr.responseURL property returns the final URL of an XMLHttpRequest instance after following all redirects. This is the only way to retrieve the Location header.

Response Type

We can use the xhr.responseType property to set the expected response format:

  • Empty (default) or text — plain text
  • json — parsed JSON
  • blob — binary data Blob
  • document — XML document
  • arraybufferArrayBuffer for binary data

Let’s call a RESTful API to get the response as JSON:

const xhr = new XMLHttpRequest()

xhr.open('GET', 'https://api.jsonbin.io/b/5d5076e01ec3937ed4d05eab/1')

// set response format
xhr.responseType = 'json'

xhr.send()

xhr.onload = () => {
  // get JSON response
  const user = xhr.response

  // log details
  console.log(user.name) // John Doe
  console.log(user.email) // john.doe@example.com
  console.log(user.website) // http://example.com
}

Request States (xhr.readyState)

The XMLHttpRequest object changes state as the request progresses. We can access the current state using the xhr.readyState property.

The states are:

  • UNSENT (0) — The initial state
  • OPENED (1) — The request begins
  • HEADERS_RECEIVED (2) — The HTTP headers received
  • LOADING (3) — Response is loading
  • DONE (4) — The request is completed

We can track the request state by using the onreadystatechange event:

xhr.onreadystatechange = function () {
  if (xhr.readyState == 1) {
    console.log('Request started.')
  }

  if (xhr.readyState == 2) {
    console.log('Headers received.')
  }

  if (xhr.readyState == 3) {
    console.log('Data loading..!')
  }
  if (xhr.readyState == 4) {
    console.log('Request ended.')
  }
}

Aborting Request

We can easily abort an XHR request anytime by calling the abort() method on the xhr object:

xhr.abort() // cancel request

Synchronous Requests

By default, XHR makes an asynchronous request which is good for performance. But if you want to make an explicit synchronous request, just pass false as 3rd argument to the open() method. It will pause the JavaScript execution at send() and resume when the response is available:

xhr.open('GET', 'https://api.jsonbin.io/b/5d5076e01ec3937ed4d05eab/1', false)

Be careful! Chrome display the following warning for synchronous XHR request: [Deprecation] Synchronous XMLHttpRequest on the main thread is deprecated because of its detrimental effects on the end user’s experience.

XMLHttpRequest allows us to set request headers and read response headers. We can set the request Content-Type & Accept headers by calling setRequestHeader() method on the xhr object:

// set request headers
xhr.setRequestHeader('Content-Type', 'application/json')
xhr.setRequestHeader('Accept', '*/*') // accept all

Similarly, if you want to read the response headers (except Set-Cookie), call get response header() on the xhr object:

// read response headers
xhr.getResponseHeader('Content-Type')
xhr.getResponseHeader('Cache-Control')

Want to get response headers at once? Use getAllResponseHeaders() instead:

xhr.getAllResponseHeaders()

XHR POST Request

There are two ways to make a POST HTTP request using XMLHttpRequest: URL encoded form-data and FormData API.

XHR POST Request with with application/x-www-form-urlencoded

The following example demonstrates how you can make a POST request with URL-encoded form data:

const xhr = new XMLHttpRequest()

// configure a `POST` request
xhr.open('POST', '/login')

// prepare form data
let params = 'username=attacomsian&password=123456'

// set `Content-Type` header
xhr.setRequestHeader('Content-Type', 'application/x-www-form-urlencoded')

// pass `params` to `send()` method
xhr.send(params)

// listen for `load` event
xhr.onload = () => {
  console.log(xhr.responseText)
}

XHR POST Request with JSON Data

To make an XHR POST request with JSON data, you must the JSON data into a string using JSON.stringify() and set the content-type header to application/json:

const xhr = new XMLHttpRequest()

// configure a `POST` request
xhr.open('POST', '/login')

// create a JSON object
const params = {
  username: 'attacomsian',
  password: '123456'
}

// set `Content-Type` header
xhr.setRequestHeader('Content-Type', 'application/json')

// pass `params` to `send()` method
xhr.send(JSON.stringify(params))

// listen for `load` event
xhr.onload = () => {
  console.log(xhr.responseText)
}

Cross-Origin Requests & Cookies

XMLHttpRequest can send cross-origin requests, but it is subjected to special security measures. To request a resource from a different server, the server must explicitly support this using CORS (Cross-Origin Resource Sharing).

Just like Fetch API, XHR does not send cookies and HTTP authorization to another origin. To send cookies, you can use the withCredentials property of the xhr object:

xhr.withCredentials = true

XHR vs. jQuery

jQuery wrapper methods like $.ajax() use XHR under the hood to provide a higher level of abstraction. Using jQuery, we can translate the above code into just a few lines:

$.ajax('https://jsonplaceholder.typicode.com/users')
  .done(data => {
    console.log(data)
  })
  .fail(err => {
    console.error('Error:', err)
  })

XHR vs. Fetch API

The Fetch API is a promise-based modern alternative to XHR. It is clean, easier to understand, and massively used in PWA Service Workers.

The XHR example above can be converted to a much simpler fetch()-based code that even automatically parses the returned JSON:

fetch('https://jsonplaceholder.typicode.com/users')
  .then(res => res.json())
  .then(json => console.log(json))
  .catch(err => console.error('Error:', err))

Read JavaScript Fetch API guide to understand how you can use Fetch API to request network resources with just a few lines of code.

✌️ Like this article? Follow me on
Twitter
and LinkedIn.
You can also subscribe to
RSS Feed.

These days I have been working on a Node.js front-end server that calls back-end APIs and renders HTML with React components. In this microservices setup, I am making sure that the server doesn’t become too slow even when its dependencies have problems. So I need to set timeouts to the API calls so that the server can give up non-essential dependencies quickly and fail fast when essential dependencies are out of order.

As I started looking at timeout options carefully, I quickly found that there were many different kinds of timeouts even in the very limited field, HTTP request with JavaScript.

Node.js http and https

Let’s start with the standard library of Node.js. http and https packages provide request() function, which makes a HTTP(S) request.

Timeouts on http.request()

http.request() takes a timeout option. Its documentation says:

timeout <number>: A number specifying the socket timeout in milliseconds. This will set the timeout before the socket is connected.

So what does it actually do? It internally calls net.createConnection() with its timeout option, which eventually calls socket.setTimeout() before the socket starts connecting.

There is also http.ClientRequest.setTimeout(). Its documentation says:

Once a socket is assigned to this request and is connected socket.setTimeout() will be called.

So this also calls socket.setTimeout().

Either of them doesn’t close the connection when the socket timeouts but only emits a timeout event.

So, what does socket.setTimeout() do? Let’s check.

net.Socket.setTimeout()

The documentation says:

Sets the socket to timeout after timeout milliseconds of inactivity on the socket. By default net.Socket does not have a timeout.

OK, but what does «inactivity on the socket» exactly mean? In a happy path, a TCP socket follows the following steps:

  1. Start connecting
  2. DNS lookup is done: lookup event (Doesn’t happen in HTTP Keep-Alive)
  3. Connection is made: connect event (Doesn’t happen in HTTP Keep-Alive)
  4. Read data or write data

When you call socket.setTimeout(), a timeout timer is created and restarted before connecting, after lookup, after connect and each data read & write. So the timeout event is emitted on one of the following cases:

  • DNS lookup doesn’t finish in the given timeout
  • TCP connection is not made in the given timeout after DNS lookup
  • No data read or write in the given timeout after connection, previous data read or write

This might be a bit counter-intuitive. Let’s say you called socket.setTimeout(300) to set the timeout as 300 ms, and it took 100 ms for DNS lookup, 100 ms for making a connection with a remote server, 200 ms for the remote server to send response headers, 50 ms for transferring the first half of the response body and another 50 ms for the rest. While the entire request & response took more than 500 ms, timeout event is not emitted at all.

Because the timeout timer is restarted in each step, timeout happens only when a step is not completed in the given time.

Then what happens if timeouts happen in all of the steps? As far as I tried, timeout event is triggered only once.

Another concern is HTTP Keep-Alive, which reuses a socket for multiple HTTP requests. What happens if you set a timeout for a socket and the socket is reused for another HTTP request? Never mind. timeout set in an HTTP request does not affect subsequent HTTP requests because the timeout is cleaned up when it’s kept alive.

HTTP Keep-Alive & TCP Keep-Alive

This is not directly related to timeout, but I found Keep-Alive options in http/https are a bit confusing. They mix HTTP Keep-Alive and TCP Keep-Alive, which are completely different things but coincidentally have the same name. For example, the options of http.Agent constructor has keepAlive for HTTP Keep-Alive and keepAliveMsecs for TCP Keep-Alive.

So, how are they different?

  • HTTP Keep-Alive reuses a TCP connection for multiple HTTP requests. It saves the TCP connection overhead such as DNS lookup and TCP slow start.
  • TCP Keep-Alive closes invalid connections, and it is normally handled by OS.

So?

http/https use socket.setTimeout() whose timer is restarted in stages of socket lifecycle. It doesn’t ensure a timeout for the overall request & response. If you want to make sure that a request completes in a specific time or fails, you need to prepare your own timeout solution.

Third-party modules

request module

request is a very popular HTTP request library that supports many convenient features on top of http/https module. Its README says:

timeout — Integer containing the number of milliseconds to wait for a server to send response headers (and start the response body) before aborting the request.

However, as far as I checked the implementation, timeout is not applied to the timing of response headers as of v2.81.1.

Currently this module emits the two types of timeout errors:

  • ESOCKETTIMEDOUT: Emitted from http.ClientRequest.setTimeout() described above, which uses socket.setTimeout().
  • ETIMEDOUT: Emitted when a connection is not established in the given timeout. It was applied to the timing of response headers before v2.76.0.

There is a GitHub issue for it, but I’m not sure if it’s intended and the README is outdated, or it’s a bug.

By the way, request provides a useful timing measurement feature that you can enable with time option. It will help you to define a proper timeout value.

axios module

axios is another popular library that uses Promise. Like request module’s README, its timeout option timeouts if the response status code and headers don’t arrive in the given timeout.

Browser APIs

While my initial interest was server-side HTTP requests, I become curious about browser APIs as I was investigating Node.js options.

XMLHttpRequest

XMLHttpRequest.timeout aborts a request after the given timeout and calls ontimeout event listeners. The documentation does not explain the exact timing, but I guess that it is until readyState === 4, which means that the entire response body has arrived.

fetch()

As far as I read fetch()‘s documentation on MDN, it does not have any way to specify a timeout. So we need to handle by ourselves. We can do that easily using Promise.race().

function withTimeout(msecs, promise) {
  const timeout = new Promise((resolve, reject) => {
    setTimeout(() => {
      reject(new Error("timeout"));
    }, msecs);
  });
  return Promise.race([timeout, promise]);
}

withTimeout(1000, fetch("https://foo.com/bar/"))
  .then(doSomething)
  .catch(handleError);

This kind of external approach works with any HTTP client and timeouts for the overall request and response. However, it does not abort the underlying HTTP request while preceding timeouts actually abort HTTP requests and save some resources.

Conclusion

Most of the HTTP request APIs in JavaScript doesn’t offer timeout mechanism for the overall request and response. If you want to limit the maximum processing time for your piece of code, you have to prepare your own timeout solution. However, if your solution relies on a high-level abstraction like Promise and cannot abort underlying TCP socket and HTTP request when timeout, it is nice to use an existing low-level timeout mechanisms like socket.setTimeout() together to save some resources.

Время прочтения
8 мин

Просмотры 5.1K

I. Суть проблемы.

В список основных предназначений XMLHttpRequest, конечно, не входит запрос HTML, чаще этот инструмент взаимодействует с XML, JSON или простым текстом.

Однако связка XMLHttpRequest + HTML хорошо работает при создании расширений к браузеру, которые в фоновом режиме опрашивают на предмет новостей сайты, не предоставляющие для этого почтовую подписку, RSS или другие экономные API или предоставляющие эти сервисы с какими-то ограничениями.

При создании нескольких расширений для Firefox я сталкивался с такой необходимостью. Работать с полученным от XMLHttpRequest кодом HTML при помощи регулярных выражений — способ очень ненадёжный и громоздкий. Получить DOM от XMLHttpRequest можно было лишь для правильного XML. Поэтому приходилось следовать хитрым советам на сайте разработчиков. Однако начиная с Firefox 11 появилась возможность непосредственного получения DOM от XMLHttpRequest, а в Firefox 12 была добавлена обработка таймаутов.

Я испытал новую возможность на создании мини-индикаторов новых топиков для двух небольших форумов, и это оказалось очень удобным (50 строчек кода плюс расширение CustomButtons — вот и готовый индикатор за пять минут, с опросами по таймеру и четырьмя состояниями: нет новостей, есть новости, ошибка и таймаут; подробнее можно почитать здесь). Всё работало как часы.

Поэтому я попытался убрать из кода своих расширений все прежние костыли и ввести туда новый удобный парсинг. Однако при работе с сайтом rutracker.org возникла странная проблема (тестирование проходит на последней ночной сборке под Windows XP; очень прошу прощения за все косяки в коде и формулировках: у меня нет программистского образования и опыт мой в этой сфере, к сожалению, очень невелик.).

Нижеследующий упрощённый пример кода почти всё время уходит в таймаут (для проверки нужно авторизоваться на сайте — далее станет понятно, почему это существенно):

var xhr = new XMLHttpRequest();
xhr.mozBackgroundRequest = true;
xhr.open("GET", "http://rutracker.org/forum/index.php", true);
xhr.timeout = 10000;
xhr.channel.loadFlags |= Components.interfaces.nsIRequest.LOAD_BYPASS_CACHE;
xhr.responseType = "document";
xhr.onload = function() {
	alert(this.responseXML.title);
}
xhr.onerror = function() {
	alert("Error!");
}
xhr.ontimeout = function() {
	alert("Timeout!");
}
xhr.send(null);

Причём загвоздка именно в парсинге HTML в DOM, потому что сайт отдаёт страницу без задержки и, например, следующий код без парсинга работает без запинок:

var xhr = new XMLHttpRequest();
xhr.mozBackgroundRequest = true;
xhr.open("GET", "http://rutracker.org/forum/index.php", true);
xhr.timeout = 10000;
xhr.channel.loadFlags |= Components.interfaces.nsIRequest.LOAD_BYPASS_CACHE;
xhr.onload = function() {
	alert(this.responseText.match(/<title>.+?</title>/i)[0]);
}
xhr.onerror = function() {
	alert("Error!");
}
xhr.ontimeout = function() {
	alert("Timeout!");
}
xhr.send(null);

Спецификация XMLHttpRequest утверждает, что при парсинге HTML/XML в DOM scripts in the resulting document tree will not be executed, resources referenced will not be loaded and no associated XSLT will be applied, то есть скрипты не отрабатываются и никакие ресурсы не загружаются (что подтверждается мониторингом HTTP активности при описанных запросах), так что с этих сторон задержки быть не может. Единственная загвоздка может быть только в структуре самого DOM: парсинг почему-то зависает и создаёт псевдо-таймаут.

II. Дополнительные наблюдения.

Тогда я создал небольшой скрипт для DOM-статистики и стал при его помощи анализировать проблемную страницу.

var doc = content.document;
var root = doc.documentElement;

var text_char = root.textContent.length;

var elm_nodes = doc.evaluate(".//*", root, null, XPathResult.ORDERED_NODE_SNAPSHOT_TYPE, null).snapshotLength;
var txt_nodes = doc.evaluate(".//text()", root, null, XPathResult.ORDERED_NODE_SNAPSHOT_TYPE, null).snapshotLength;
var com_nodes = doc.evaluate(".//comment()", root, null, XPathResult.ORDERED_NODE_SNAPSHOT_TYPE, null).snapshotLength;
var all_nodes = doc.evaluate(".//node()", root, null, XPathResult.ORDERED_NODE_SNAPSHOT_TYPE, null).snapshotLength;

var max_nst_lv = 0;
var max_nst_lv_nodes = 0;
for (var level = 1, pattern = "./node()"; level <= 50; level++, pattern += "/node()") {
	var elm_num = doc.evaluate(pattern,root,null,XPathResult.ORDERED_NODE_SNAPSHOT_TYPE,null).snapshotLength;
	if (elm_num) {
		max_nst_lv = level;
		max_nst_lv_nodes = elm_num;
	}
}

alert(
	text_char + "ttext charactersnn" +
	
	elm_nodes + "telement nodesn" +
	txt_nodes + "ttext nodesn" +
	com_nodes + "tcomment nodesn" +
	all_nodes + "tall nodesnn" +
	
	max_nst_lv_nodes + " nodes in the " + max_nst_lv + " maximum nesting leveln"
);

Вот некоторые ещё более озадачившие меня данные.

1. Заглавная страница форума с отключённым JavaScript имеет: 49677 знаков в текстовых узлах, 4192 HTML элементов, 4285 текстовых узлов, 77 комментариев, всего 8554 узлов; 577 узлов на максимальном 25-м уровне вложенности узлов.

2. Если выйти из форума и загрузить страницу для неавторизованных пользователей, получится такая статистика: 47831 знаков в текстовых узлах, 3336 HTML элементов, 4094 текстовых узлов, 73 комментариев, всего 7503 узлов; 1136 узлов на максимальном 24-м уровне вложенности узлов. Структура явно проще и если испробовать проблемный код, выйдя из форума (то есть на этой странице для неавторизованных пользователей), то никаких таймаутов не происходит.

3. Попробовал загружать проблемную страницу на испытательный сайт и понемногу упрощать её структуру. Например, если удалить все элементы td с классом row1 (заголовки форумов и субфорумов в таблице на заглавной странице) и больше ничего не менять, получим такую статистику: 20450 знаков в текстовых узлах, 1355 HTML элементов, 1726 текстовых узлов, 77 комментариев, всего 3158 узлов; 8 узлов на максимальном 25-м уровне вложенности узлов. И опять-таки данная страница за очень редким исключением не даёт таймаутов.

4. Очень странное значение имеют элементы script. На заглавной странице их 19 (в head и body вместе взятых, загружаемых и встроенных). Если удалить только эти элементы, страница перестаёт давать таймауты. Причём если удалять от конца до начала, нужно удалять все (даже если оставить первый загружаемый скрипт в head, таймауты продолжаются). А если удалять от начала до конца, таймауты прекращаются после удаления скрипта, встроенного в элемент p класса forum_desc hidden в разделе «Правила, основные инструкции, FAQ-и», после него можно оставить ещё 6 скриптов, и таймауты всё равно прекратятся (причём удаление только этого скрипта проблему не решает). Причём если все 19 скриптов заменить пустыми элементами script без кода и без атрибута src, таймауты остаются. Но если эти пустые элементы заменить на такие же пустые элементы style в том же количестве, таймауты сразу пропадают.

5. При помощи скрипта на PERL попробовал создать тестовый HTML с более-менее сложной структурой (но без элементов script). Получился файл размером почти в 10 мегабайт со следующей статистикой: 9732505 знаков в текстовых узлах, 25004 HTML элементов, 25002 текстовых узлов, 1000 комментариев, всего 51006 узлов; 1000 узлов на максимальном 27-м уровне вложенности. Вроде бы структура объёмнее и сложнее проблемной страницы, однако никаких таймаутов она не вызывает. Стало очевидным, что дело в каком-то неоднозначном сочетании объёма/сложности/специфики элементов.

6. Стоило только добавить к этой смоделированной странице элементы script, таймауты вернулись (хоть порог таймаута я увеличил в этом сложном случае до минуты).

III. Создание легко воспроизводимого прецедента.

У меня получилось достичь некоторого критического минимума проблемности структуры, соизмеримой со структурой заглавной страницы трекера, при помощи такого скрипта на PERL:

use strict;
use warnings;

open(OUTPUT, '>:raw:encoding(UTF-8)', "test.html") or die "Cannot write to test.html: $!n";
print OUTPUT
	"<!DOCTYPE html PUBLIC '-//W3C//DTD HTML 4.01 Transitional//EN' 'http://www.w3.org/TR/html4/loose.dtd'>n" .
	"<html><head><meta http-equiv='Content-Type' content='text/html; charset=UTF-8'><title>Test</title></head><body>" .
	 (("<div class='abcd'>abcd" x 25 . "</div>" x 25 ) x 10 . "<script type='text/javascript'>var a = 1234;</script>") x 20 .
	"</body></html>n";
close(OUTPUT);
 

Статистика страницы: 20265 знаков в текстовых узлах, 5024 HTML элементов, 5022 текстовых узлов, 0 комментариев, всего 10046 узлов; 200 узлов на максимальном 27-м уровне вложенности узлов. В том числе 20 простейших элементов script. Получаем 10 таймаутов из 10 попыток.

При разных попытках упростить структуру или сократить объём вероятность таймаутов снижается, но довольно непредсказуемым образом (ни одно из указанных упрощений не накладывалось на другое, перед каждым скрипт возвращался к исходному коду):

— перемещение всех элементов script в конец кода (при том, что больше ничего не меняется и статистика остаётся прежней): 0 таймаутов из 10 попыток.
— замена элементов script на элементы span с одним атрибутом и тем же текстовым содержимым (без перемещения в конец): 0 таймаутов из 10 попыток.
— сокращения текста скрипта на 3 знака: 7 таймаутов из 10.
— удаления всего содержимого скрипта (остаётся только пустой тег): 6 таймаутов из 10 попыток.
— сокращение текста элементов div до одного знака: 5 таймаутов из 10 попыток.
— полное удаление текста элементов div (получается пустая страница): 7 таймаутов из 10 попыток.
— сокращение атрибута class элементов div до одного знака: 8 таймаутов из 10 попыток.
— удаление атрибута class элементов div: 1 таймаут из 10 попыток.
— сокращение количества элементов script до 2 (в середине кода и в конце): опять 10 таймаутов из 10 попыток.
— сокращение количества элементов script до 1 (в начале кода): всё те же 10 таймаутов из 10 попыток (но если этот элемент переместить в конец кода, таймауты пропадают совершенно).
— сокращение количества элементов div (и соответственно текстовых узлов) наполовину с сохранением максимального уровня вложенности: 3 таймаута из 10 попыток.
— сокращение максимального уровня вложенности наполовину (общее количество элементов и текстовых узлов остаётся почти тем же, но вдвое вырастает количество элементов на максимальном уровне вложенности): 7 таймаутов из 10 попыток.
— сокращение максимального уровня вложенности всего до 3 (body/div/текст или body/script/текст) с сохранением общего количества элементов: 8 таймаутов из 10 попыток.

IV. Предварительные выводы.

Во всех описанных случаях никакой перегрузки процессора не наблюдалось, так что нет оснований винить в зависаниях аппаратную часть (как и задержки в сети: код получается за доли секунды, в браузере страница рендерится за время значительно меньшее таймаутов). Очевидно, в XMLHttpRequest под парсинг HTML в DOM выделяются какие-то ограниченные ресурсы, которые исчерпываются разным сочетанием параметров. Причём загадочную роль играют элементы script (которые даже не исполняются) и особенно их порядок в коде. Если этот верно, стоит увеличить ресурсы и снизить странную зависимость от вида и порядка элементов, поскольку проблема отнюдь не надуманная и возникает в ходе обычной разработки расширений.

V. Что дальше.

Когда я только начинал анализировать проблему и спросил совета на нескольких сайтах, на forums.mozilla.org администратор предположил баг в сфере производительности и посоветовал отослать сообщение на bugzilla.mozilla.org в раздел Core::DOM с описанием воспроизводимой ситуации. Тогда я имел ещё очень мало данных, да и сейчас они очень неясны. Поэтому буду благодарен за любые соображения, позволяющие конкретизировать проблему и внятно её сформулировать. Иначе ведь придётся переводить всю эту простыню на английский (что мне при моём уровне владения языком и материалом будет сделать очень непросто) и постить на bugzilla.mozilla.org как есть, что, конечно, будет проявлением неуважительности к чужому времени.

Понравилась статья? Поделить с друзьями:

Читайте также:

  • Xmlhttprequest error flutter web
  • Xmlhttprequest cors error
  • Xml файл не прошел форматный контроль ошибка 503 как исправить
  • Xml validation error
  • Xml serializer error

  • 0 0 голоса
    Рейтинг статьи
    Подписаться
    Уведомить о
    guest

    0 комментариев
    Старые
    Новые Популярные
    Межтекстовые Отзывы
    Посмотреть все комментарии