Your country

Tools that support it use your country for local currency, number formats, units and paper size. Your choice is saved only in this browser.

Type a name or a two-letter code. Use the up and down arrow keys to move through the countries, Enter to choose one and Escape to close.

jQuery 4 (legacy code and migration to DOM APIs)  Module 4 – Ajax, Deferreds and forms

Ajax with jQuery: $.ajax, get, post and load

What $.ajax, $.get, $.post and .load() send and receive: form, JSON and FormData bodies, a CSRF header, and how 404s, bad JSON and timeouts reach fail().

  • Intermediate
  • 25 minutes
  • Examples run with Node.js 24.21.0, jQuery 4.0.0, quickjs 0.32.0 and PHP 8.5.11
  • By MySmartCoPilot
Free lesson, Pro answers: 25 minutes
Free: the whole lesson. With a pass: the model answers to its 2 interview questions.

What you will learn

  • Read the settings of a $.ajax call and say what its request carries, from the method and URL to the headers
  • Predict the body and Content-Type that jQuery sends for an object holding a list, for JSON text and for FormData
  • Handle HTTP errors, timeouts, aborts, parse errors and empty answers in fail(), with a message for each
  • Send a CSRF token in a header to your own origin only, and name the servers that read X-Requested-With

Before you start

What you’ll learn · Before you start

What you will learn

  • Read the settings of a $.ajax call and say what its request carries, from the method and URL to the headers
  • Predict the body and Content-Type that jQuery sends for an object holding a list, for JSON text and for FormData
  • Handle HTTP errors, timeouts, aborts, parse errors and empty answers in fail(), with a message for each
  • Send a CSRF token in a header to your own origin only, and name the servers that read X-Requested-With

Before you start

On this page

An enquiry form sends { name: 'Asha K', services: ['web', 'seo'] } with $.post(), and the PHP script behind it receives name=Asha+K&services%5B%5D=web&services%5B%5D=seo: a form body, not JSON. Change one setting and the same enquiry arrives as JSON text that PHP turns into a single meaningless key, and neither side reports an error.

Every request in this lesson comes from a shop page that ran in jsdom with jQuery 4.0.0, most of them with 3.7.1 as well, and went to a test server that wrote down what it received: each section shows a request as a server sees it. You will read what a call sends, predict the body and Content-Type for objects, JSON, arrays and FormData, handle every way a request can fail, and send a CSRF token without breaking the calls a page makes to other sites.

$.get(), $.post() and .load() are $.ajax() with settings filled in

Every Ajax function of jQuery ends in one call. $.get(url, data, success) is $.ajax() with method: 'GET', $.post() is the same with method: 'POST', and $.getJSON() is a GET with dataType: 'json' (jQuery.get(), jQuery.post(), jQuery.getJSON()). .load(url) is a method of a collection instead: it fetches HTML, with a GET, or a POST when its data is an object, and puts the answer into the selected elements (.load()). Old code reads more easily once each shorthand is written out as the settings it stands for:

Shorthand The same request with $.ajax()
$.get(url, data, success) { url, data, success, method: 'GET' }
$.getJSON(url, data, success) { url, data, success, method: 'GET', dataType: 'json' }
$.post(url, data, success) { url, data, success, method: 'POST' }
$(el).load(url, data, done) a GET, or a POST when data is an object, with dataType: 'html'; the answer goes into el

The shop’s stock page makes each call once. The test server logged every request, and this run prints the Accept header as well:

The stock page and shorthands.js

shop/shorthands.html

<!doctype html>
<html lang="en">
<head>
  <meta charset="utf-8">
  <title>Stock and offers</title>
</head>
<body>
  <section id="deals"></section>
  <section id="all"></section>
  <script src="https://code.jquery.com/jquery-4.0.0.min.js" integrity="sha256-OaVG6prZf4v69dPg6PhVattBXkcOWQB62pdZ3ORyrao=" crossorigin="anonymous"></script>
  <script src="shorthands.js"></script>
</body>
</html>

shop/shorthands.js

// The shop page's shorthand calls, one after another (each is awaited, so the log stays in order), and what each
// one handed to its callback. The test server logs what each request carried.
const loaded = ($target, url) =>
  new Promise((done) => {
    $target.load(url, (text, textStatus) => {
      console.log(`.load('${url}'): ${textStatus}, the section now holds ${$target.html()}`);
      done();
    });
  });

(async () => {
  await $.get('/srv/stock', { sku: 'K-7310' }, (data, textStatus) => {
    console.log(`$.get: ${textStatus}, data is an ${typeof data}: ${JSON.stringify(data)}`);
  });
  await $.getJSON('/srv/stock', { sku: 'L-0042' }).done((data) => {
    console.log(`$.getJSON: ${data.sku} has ${data.inStock} in stock`);
  });
  await $.post('/srv/echo', { sku: 'K-7310', qty: 2 }, (data) => {
    console.log(`$.post: ${JSON.stringify(data)}`);
  });
  await loaded($('#deals'), '/srv/offers #offers');
  await loaded($('#all'), '/srv/offers');
})();
What shorthands.js logged, and what the server received JavaScript · shorthands.mjs
// Prints what shop/shorthands.js logged in jsdom and what the test server received (recorded.mjs).
import { show } from './show.mjs';

show('shop/shorthands.html', { accept: true });

Output

server: GET /srv/stock?sku=K-7310 from XMLHttpRequest, answered 200
  X-Requested-With: XMLHttpRequest
  Accept: */*
$.get: success, data is an object: {"sku":"K-7310","inStock":4}
server: GET /srv/stock?sku=L-0042 from XMLHttpRequest, answered 200
  X-Requested-With: XMLHttpRequest
  Accept: application/json, text/javascript, */*; q=0.01
$.getJSON: L-0042 has 4 in stock
server: POST /srv/echo from XMLHttpRequest, answered 200
  Content-Type: application/x-www-form-urlencoded; charset=UTF-8
  X-Requested-With: XMLHttpRequest
  Accept: */*
  body: sku=K-7310&qty=2
$.post: {"ok":true}
server: GET /srv/offers from XMLHttpRequest, answered 200
  X-Requested-With: XMLHttpRequest
  Accept: text/html, */*; q=0.01
.load('/srv/offers #offers'): success, the section now holds <div id="offers"><p>Delivery in 2 days</p></div>
server: GET /srv/offers from XMLHttpRequest, answered 200
  X-Requested-With: XMLHttpRequest
  Accept: text/html, */*; q=0.01
the script in offers ran
.load('/srv/offers'): success, the section now holds <div id="offers"><p>Delivery in 2 days</p></div><p id="other">Not wanted here</p><script>console.log("the script in offers ran")</script>
(jQuery 3.7.1 logged the same 23 lines)

Recorded with Node.js 24.21.0 on macOS 26 arm64. To run it yourself: mise exec node@24.21.0 -- node shorthands.mjs

$.get() handed its callback an object, not text: no dataType was given, so jQuery chose one from the answer’s Content-Type, application/json, and parsed the body (jQuery.ajax()). $.getJSON() said what it wanted in its Accept header, and .load() asked for text/html. $.post() sent its object as a form body, sku=K-7310&qty=2, which the next section takes apart. Every request also carried X-Requested-With: XMLHttpRequest, a header that jQuery adds itself and that the last section comes back to.

The two .load() calls fetched the same fragment and treated its script differently. With a selector after the URL, '/srv/offers #offers', jQuery kept only that element and removed the scripts before inserting it. Without one, the whole fragment went in through .html(), and its script ran: the run logged “the script in offers ran” before the callback, as the documentation of .load() describes.

Point .load() only at HTML you trust

.load(url) without a selector runs every script in the answer, so use it only for your own server’s HTML, and add a selector when you want one element of a page. Text that users typed never goes in through it, or through .html(): insert that with .text().

Checkpoint

Check: The shorthands and the settings they fill in

  1. Question 1 of 2 Which $.ajax() call sends the same request as $.getJSON("/srv/stock", { sku: "L-0042" })?

    Choose one answer.

    Show the answer: Question 1 of 2

    Answer: $.ajax({ url: "/srv/stock", data: { sku: "L-0042" }, dataType: "json" })

    $.getJSON(url, data, success) is $.ajax() with method GET and dataType "json". The data object becomes the query string, and the dataType sets the Accept header and makes jQuery parse the answer.

  2. Question 2 of 2 The fragment /srv/offers holds <div id="offers">, another paragraph and a script. What does $("#all").load("/srv/offers") do with the script?

    Choose one answer.

    Show the answer: Question 2 of 2

    Answer: Inserts the whole fragment through .html(), and the script runs

    With a selector, .load() keeps the matching elements and drops the scripts; without one, the scripts of the fragment run. Point .load() only at HTML from your own server.

What the server receives depends on data, contentType and processData

The data setting is turned into a request in one of three ways. A plain object goes through $.param(), which writes the bracket names that PHP reads, services[] for each item of a list, and is sent with the default contentType, application/x-www-form-urlencoded; charset=UTF-8 (jQuery.ajax()). A string is sent as it is, still labelled with that default type unless you change contentType; under the form type, jQuery’s one change is to turn each %20 in it into + (src/ajax.js). And with processData: false, jQuery leaves the data alone, which is meant for objects the browser sends itself, such as FormData. Step through one enquiry sent with five settings, and watch what reaches PHP:

Interactive · message by message One enquiry, five settings, and what PHP reads Step through the enquiry as each setting sends it, and watch what reaches $_POST on the server.
One enquiry, five settings, and what PHP reads, step 7 of 7. Messages so far: The enquiry page to The request: name=Asha+K&services%5B%5D…; The request to PHP's $_POST: three fields; The enquiry page to PHP's $_POST: {"name":"Asha K",…}; The enquiry page to PHP's $_POST: Content-Type: application/json; The enquiry page to PHP's $_POST: [object Object]; The enquiry page to PHP's $_POST: multipart/form-data.The enquiry pagename, services, budgetThe requestmultipart, one part eachPHP's $_POSTname; the file in $_FILESname=Asha+K&services%5B%5D…three fields{"name":"Asha K",…}Content-Type: application/json[object Object]multipart/form-data

The drawing shows the whole scene; its 7 steps are listed below.

Every step as text
  1. The enquiry. The page holds one enquiry: name "Asha K", services, a list of web and seo, and budget, the number 50000. Each step sends it with different $.ajax settings.
  2. An object. $.post(url, enquiry) encodes the object with $.param(): services becomes services[]=web&services[]=seo, the space a +, and the Content-Type says it is a form.
  3. PHP reads a form. PHP fills $_POST from a form body: name is "Asha K" and services a list of two. The budget arrives as the string "50000", because a form body holds only text.
  4. JSON text as a form. data: JSON.stringify(enquiry) without a contentType sends JSON text, while the Content-Type still says form. PHP parses the JSON as a form and builds one odd key: $_POST["name"] is not set.
  5. JSON as JSON. Add contentType "application/json" and the label matches the body. $_POST stays empty for a JSON body; the PHP script reads php://input and json_decode() gives the enquiry, budget as a number.
  6. processData false. processData: false tells jQuery to leave data alone. With a plain object nothing encodes it, so the body is the text [object Object] and $_POST is empty.
  7. FormData. A FormData object goes as multipart/form-data, each field and the file brief.txt in a part of its own between boundary lines. PHP puts the fields in $_POST and the file in $_FILES.

The enquiry page makes those five requests, then two GET requests, and the test server logged the Content-Type and the body of each:

The enquiry page and bodies.js

shop/bodies.html

<!doctype html>
<html lang="en">
<head>
  <meta charset="utf-8">
  <title>Enquiry bodies</title>
</head>
<body>
  <script src="https://code.jquery.com/jquery-4.0.0.min.js" integrity="sha256-OaVG6prZf4v69dPg6PhVattBXkcOWQB62pdZ3ORyrao=" crossorigin="anonymous"></script>
  <script src="bodies.js"></script>
</body>
</html>

shop/bodies.js

// One enquiry sent five ways, then two GET requests. The test server logs the Content-Type and the body of each.
const enquiry = { name: 'Asha K', services: ['web', 'seo'], budget: 50000 };

(async () => {
  // 1. A plain object: jQuery encodes it with $.param() as a form body.
  await $.post('/srv/echo', enquiry);
  // 2. JSON text, while the Content-Type still says it is a form.
  await $.ajax({ url: '/srv/echo', method: 'POST', data: JSON.stringify(enquiry) });
  // 3. JSON text with a JSON Content-Type.
  await $.ajax({ url: '/srv/echo', method: 'POST', contentType: 'application/json', data: JSON.stringify(enquiry) });
  // 4. processData: false with a plain object, so nothing encodes it.
  await $.ajax({ url: '/srv/echo', method: 'POST', processData: false, data: enquiry });
  // 5. A FormData object with a file in it.
  const form = new FormData();
  form.append('name', enquiry.name);
  form.append('brief', new Blob(['Two pages, in Hindi and English'], { type: 'text/plain' }), 'brief.txt');
  await $.ajax({ url: '/srv/echo', method: 'POST', data: form });
  // GET: the data goes into the query string, and cache: false adds a number to the URL.
  await $.get('/srv/echo', { name: enquiry.name, ids: [3, 4] });
  await $.ajax({ url: '/srv/echo', cache: false });
  console.log('sent: 5 bodies, then 2 GET requests');
})();
What the server received from bodies.js JavaScript · bodies.mjs
// Prints what shop/bodies.js logged in jsdom and what the test server received (recorded.mjs).
import { show } from './show.mjs';

show('shop/bodies.html');

Output

server: POST /srv/echo from XMLHttpRequest, answered 200
  Content-Type: application/x-www-form-urlencoded; charset=UTF-8
  X-Requested-With: XMLHttpRequest
  body: name=Asha+K&services%5B%5D=web&services%5B%5D=seo&budget=50000
server: POST /srv/echo from XMLHttpRequest, answered 200
  Content-Type: application/x-www-form-urlencoded; charset=UTF-8
  X-Requested-With: XMLHttpRequest
  body: {"name":"Asha K","services":["web","seo"],"budget":50000}
server: POST /srv/echo from XMLHttpRequest, answered 200
  Content-Type: application/json
  X-Requested-With: XMLHttpRequest
  body: {"name":"Asha K","services":["web","seo"],"budget":50000}
server: POST /srv/echo from XMLHttpRequest, answered 200
  Content-Type: application/x-www-form-urlencoded; charset=UTF-8
  X-Requested-With: XMLHttpRequest
  body: [object Object]
server: POST /srv/echo from XMLHttpRequest, answered 200
  Content-Type: multipart/form-data; boundary=BOUNDARY
  X-Requested-With: XMLHttpRequest
  body: --BOUNDARY
        Content-Disposition: form-data; name="name"
        
        Asha K
        --BOUNDARY
        Content-Disposition: form-data; name="brief"; filename="brief.txt"
        Content-Type: text/plain
        
        Two pages, in Hindi and English
        --BOUNDARY--
        
server: GET /srv/echo?name=Asha%20K&ids%5B%5D=3&ids%5B%5D=4 from XMLHttpRequest, answered 200
  X-Requested-With: XMLHttpRequest
server: GET /srv/echo?_=<number> from XMLHttpRequest, answered 200
  X-Requested-With: XMLHttpRequest
sent: 5 bodies, then 2 GET requests

Recorded with Node.js 24.21.0 on macOS 26 arm64. To run it yourself: mise exec node@24.21.0 -- node bodies.mjs

The object became name=Asha+K&services%5B%5D=web&services%5B%5D=seo&budget=50000. The space is a +, because jQuery turns the %20 of $.param() into + in a form body, the form encoding of the URL Standard, a step that moved into $.ajax() in jQuery 3.0 (3.0 upgrade guide). The JSON string of the second request went out unchanged, under the form’s Content-Type, and only the third request labelled it application/json. processData: false with a plain object sent the text [object Object]. The FormData object went as multipart/form-data, with the file in a part of its own; jQuery 4 needs no extra setting for that.

JSON text with the form's Content-Type

Wrong: $.ajax({ method: 'POST', data: JSON.stringify(enquiry) }), which labels JSON as a form. Right: add contentType: 'application/json', so the label matches the body, or pass the object itself when the server reads form fields.

The GET requests put their data in the query string instead, where the space stayed %20, and cache: false added _= and a number that changes with every request, so no cache along the way can answer it.

FormData on jQuery 3

jQuery 3 encodes every object with $.param(), FormData included, and throws, so its code sends FormData with processData: false and contentType: false. jQuery 4 sets both for FormData itself; the jQuery 4 Ajax lesson runs that call on both versions.

PHP fills $_POST only from a form body or a multipart body, and parses a form body as parse_str() parses a query string ($_POST, parse_str). A JSON body leaves $_POST empty, and the script reads it from php://input. This program decodes the bodies the server received:

What PHP makes of three of the bodies PHP · decode.php
<?php

// decode.php: what PHP makes of three of the bodies that bodies.js sent (bodies.mjs prints them). For a body sent as
// application/x-www-form-urlencoded, PHP fills $_POST as parse_str() parses a query string; a JSON body leaves $_POST
// empty, and the script reads it from php://input with json_decode() instead.

$formBodies = [
    'the object' => 'name=Asha+K&services%5B%5D=web&services%5B%5D=seo&budget=50000',
    'JSON text sent as a form' => '{"name":"Asha K","services":["web","seo"],"budget":50000}',
    'processData: false' => '[object Object]',
];

foreach ($formBodies as $from => $body) {
    parse_str($body, $post);
    echo $from, ': $_POST = ', json_encode($post, JSON_UNESCAPED_SLASHES), "\n";
    echo '  $_POST[\'name\'] is ', isset($post['name']) ? '"' . $post['name'] . '"' : 'not set', "\n";
}

$json = '{"name":"Asha K","services":["web","seo"],"budget":50000}';
$data = json_decode($json, true, 16, JSON_THROW_ON_ERROR);
echo 'JSON with a JSON Content-Type: $_POST = [], json_decode() gives ', json_encode($data, JSON_UNESCAPED_SLASHES), "\n";
echo '  budget is ', get_debug_type($data['budget']), ' here, and ', get_debug_type('50000'), " in a form body\n";

Output

the object: $_POST = {"name":"Asha K","services":["web","seo"],"budget":"50000"}
  $_POST['name'] is "Asha K"
JSON text sent as a form: $_POST = {"{\"name\":\"Asha_K\",\"services\":":{"\"web\",\"seo\"":""}}
  $_POST['name'] is not set
processData: false: $_POST = []
  $_POST['name'] is not set
JSON with a JSON Content-Type: $_POST = [], json_decode() gives {"name":"Asha K","services":["web","seo"],"budget":50000}
  budget is int here, and string in a form body

Recorded with PHP 8.5.11 (cli) (NTS) on macOS 26 arm64. To run it yourself: php decode.php

The form body gave PHP all three fields, the budget as the string "50000", since a form body holds only text. The JSON text sent as a form gave it one odd key, the JSON up to its first [ with the space turned into _, so $_POST['name'] was not set, and [object Object] gave it nothing. None of these requests failed: the server answered 200 to every one.

Key idea

A server reads a body by its Content-Type, so the label must match the bytes: an object becomes a form body, a string goes as it is under whatever contentType says, and processData: false sends only what the browser can send itself.

Checkpoint

Check: What data, contentType and processData send

  1. Question 1 of 2 A form sends $.ajax({ url: "/srv/enquiry", method: "POST", data: JSON.stringify(enquiry) }), and the PHP script reads $_POST["name"]. What does it find?

    Choose one answer.

    Show the answer: Question 1 of 2

    Answer: Nothing: PHP parses the JSON text as a form body, and name is not set

    Add contentType "application/json" and read php://input with json_decode() on the server, or send the object itself when the server reads form fields.

  2. Question 2 of 2 What body does $.ajax({ method: "POST", processData: false, data: { name: "Asha K" } }) send?

    Choose one answer.

    Show the answer: Question 2 of 2

    Answer: [object Object]

    processData false is for data the browser sends as it is, such as FormData, a Blob or a string you have encoded yourself; a plain object needs processing.

Every status outside 200–299, a bad JSON body and a timeout end in fail()

A request succeeds when its status is from 200 to 299, or 304, and its body converts to the dataType it asked for (src/ajax.js); anything else calls the fail() callbacks with three values: the jqXHR, a textStatus and an errorThrown (jQuery.ajax()). The jqXHR: The object that $.ajax() and its shorthands return: a promise of the request's result that also holds the status, the response text and the headers, with abort() to cancel the request. is the object $.ajax() returns, a promise of the result that also holds the status and the response text. Follow each of the page’s requests down the column:

Interactive · stage by stage How one request ends in done() or in fail() Step down the column with each of the page's requests, and watch where each one leaves it.
How one request ends in done() or in fail(), step 7 of 7. Showing Request sent, An answer arrives, Status 200–299 or 304?, Convert by dataType, done() and then(), fail() and catch(). In focus: done() and then()."timeout", status 0"error", status 404"parsererror"Request sentthe timeout clock startsAn answer arrivesor the clock runs outStatus 200–299 or 304?else textStatus "error"Convert by dataTypejson: JSON.parse()done() and then()data, textStatus, jqXHRfail() and catch()jqXHR, textStatus, error

The drawing shows the whole scene; its 7 steps are listed below.

Every step as text
  1. The request leaves. $.ajax() sends the request and, when the settings give a timeout, starts a clock. abort() or the clock can end the request before any answer arrives.
  2. The clock runs out. The page asked /srv/slow with timeout: 100, and the server needed 300 ms. jQuery stopped waiting and called fail() with status 0, textStatus "timeout" and errorThrown "timeout".
  3. A 404 arrives. For /srv/missing the server answered 404. Any status outside 200–299, other than 304, sends the request to fail() with textStatus "error" and errorThrown "Not Found", the status text.
  4. A 500 is a failure too. The 500 of /srv/broken went the same way: status 500, textStatus "error", errorThrown "Internal Server Error". The body of a failed request is not passed on as data; it stays in jqXHR.responseText.
  5. 200, but an empty body. For /srv/empty with dataType "json" the status was 200, so the answer went on to be converted. JSON.parse() threw on the empty body, and fail() got textStatus "parsererror".
  6. 204 No Content. The POST to /srv/subscribe got 204. jQuery does not parse a body that is not there: done() ran with textStatus "nocontent" and data undefined, even with dataType "json".
  7. A success. A status of 200–299 or 304 and a body that converts reach done() and then(), with the data, "success" and the jqXHR. Every other path above ended in fail().

The error page makes those requests one after another. Its fail() callbacks log the three values, and a handler on document logs every failure a second time:

The error page and errors.js

shop/errors.html

<!doctype html>
<html lang="en">
<head>
  <meta charset="utf-8">
  <title>When a request goes wrong</title>
</head>
<body>
  <script src="https://code.jquery.com/jquery-4.0.0.min.js" integrity="sha256-OaVG6prZf4v69dPg6PhVattBXkcOWQB62pdZ3ORyrao=" crossorigin="anonymous"></script>
  <script src="errors.js"></script>
</body>
</html>

shop/errors.js

// Requests that do not succeed, each with a fail() callback that logs the three values jQuery passes it, and a
// handler on document that sees every failure of a request whose global setting is left at true.
$(document).on('ajaxError', (event, jqXHR, settings) => {
  console.log(`  ajaxError on document: ${settings.url}, status ${jqXHR.status}`);
});
const report = (label) => (jqXHR, textStatus, errorThrown) => {
  console.log(`${label}: status ${jqXHR.status}, textStatus "${textStatus}", errorThrown "${errorThrown}"`);
};
// Waits until a request has finished, whatever the outcome, so that the lines stay in order. done() gets no argument:
// a promise resolved with the jqXHR itself would take on the request's failure.
const finished = (jqXHR) => new Promise((done) => jqXHR.always(() => done()));

(async () => {
  await finished($.ajax('/srv/missing').fail(report('404')));
  await finished($.ajax('/srv/broken').fail(report('500')));
  await finished($.ajax({ url: '/srv/empty', dataType: 'json' }).fail(report('empty body as JSON')));
  await finished($.ajax({ url: '/srv/slow', timeout: 100 }).fail(report('timeout of 100 ms')));
  await finished($.ajax({ url: '/srv/missing', global: false }).fail(report('404 with global: false')));
  const pending = $.ajax('/srv/slow').fail(report('abort() after 50 ms'));
  setTimeout(() => pending.abort(), 50);
  await finished(pending);
  await finished(
    $.ajax({ url: '/srv/subscribe', method: 'POST', dataType: 'json' }).done((data, textStatus, jqXHR) => {
      console.log(`204: done() ran, status ${jqXHR.status}, textStatus "${textStatus}", data ${data}`);
    }),
  );
})();
What errors.js logged, and what the server received JavaScript · errors.mjs
// Prints what shop/errors.js logged in jsdom and what the test server received (recorded.mjs).
import { show } from './show.mjs';

show('shop/errors.html');

Output

server: GET /srv/missing from XMLHttpRequest, answered 404
  X-Requested-With: XMLHttpRequest
404: status 404, textStatus "error", errorThrown "Not Found"
  ajaxError on document: /srv/missing, status 404
server: GET /srv/broken from XMLHttpRequest, answered 500
  X-Requested-With: XMLHttpRequest
500: status 500, textStatus "error", errorThrown "Internal Server Error"
  ajaxError on document: /srv/broken, status 500
server: GET /srv/empty from XMLHttpRequest, answered 200
  X-Requested-With: XMLHttpRequest
empty body as JSON: status 200, textStatus "parsererror", errorThrown "SyntaxError: Unexpected end of JSON input"
  ajaxError on document: /srv/empty, status 200
server: GET /srv/slow from XMLHttpRequest, answered 200
  X-Requested-With: XMLHttpRequest
timeout of 100 ms: status 0, textStatus "timeout", errorThrown "timeout"
  ajaxError on document: /srv/slow, status 0
server: GET /srv/missing from XMLHttpRequest, answered 404
  X-Requested-With: XMLHttpRequest
404 with global: false: status 404, textStatus "error", errorThrown "Not Found"
server: GET /srv/slow from XMLHttpRequest, answered 200
  X-Requested-With: XMLHttpRequest
abort() after 50 ms: status 0, textStatus "abort", errorThrown "abort"
  ajaxError on document: /srv/slow, status 0
server: POST /srv/subscribe from XMLHttpRequest, answered 204
  X-Requested-With: XMLHttpRequest
204: done() ran, status 204, textStatus "nocontent", data undefined
(jQuery 3.7.1 logged the same 26 lines)

Recorded with Node.js 24.21.0 on macOS 26 arm64. To run it yourself: mise exec node@24.21.0 -- node errors.mjs

The request status textStatus, errorThrown
404 or 500 the status "error", the status text (empty over HTTP/2)
200, empty body, dataType: 'json' 200 "parsererror", the SyntaxError
No answer within timeout 0 "timeout", "timeout"
abort() before the answer 0 "abort", "abort"
204 No Content 204, in done() "nocontent", no error

Two of those rows surprise people. A 200 can fail: the documentation says an empty answer is rejected for JSON, and the server should send null or {} instead. And a 204 succeeds with data undefined, because there is no body to parse. For a parse error, errorThrown is the SyntaxError that JSON.parse() threw, and for an HTTP error it is the status text, which an answer over HTTP/2 does not carry, so there it is an empty string (jQuery.ajax()). Branch on textStatus and the status, never on errorThrown.

The handler on document saw every failure except the one sent with global: false. Global events such as ajaxError, ajaxStart and ajaxStop are attached with .on(): the shorthand methods .ajaxError() and .ajaxStart() were deprecated in jQuery 3.5 (.ajaxStart()), and .on() is what the ajaxError event page shows. A global handler suits logging; the message a form shows belongs in the request’s own fail(), next to the form.

One message for each way to fail

Read textStatus first, then the status: a timeout, an abort and a lost connection all have status 0, and a parse error has status 200. The practice section below builds that function.

Checkpoint

Check: How a request ends in fail()

  1. Question 1 of 2 A request with dataType "json" gets status 200 and an empty body. Where does it end?

    Choose one answer.

    Show the answer: Question 1 of 2

    Answer: In fail(), with textStatus "parsererror"

    jQuery's documentation says an empty answer is rejected for JSON, and that the server should send null or {}. Send 204 when there is nothing to return.

  2. Question 2 of 2 A fail() callback gets status 0. Which test tells a timeout from a lost connection?

    Choose one answer.

    Show the answer: Question 2 of 2

    Answer: textStatus === "timeout"

    Read textStatus first: "timeout", "abort" and "parsererror" each have their own message, and only then the status, where 0 means no answer arrived.

A CSRF token belongs in a header, sent to your own origin only

A CSRF token: A secret value that a server puts in its own pages and checks on every request that changes data. A request forged by another site cannot know it, so the server refuses that request. proves that a request comes from the site’s own page: the server puts a secret value in the page and refuses any request that changes data without it. OWASP’s cheat sheet recommends sending it from JavaScript in a custom request header, which the server then checks (CSRF Prevention Cheat Sheet). Laravel’s documentation puts the token in a meta tag and adds it to every jQuery request with $.ajaxSetup({ headers: { 'X-CSRF-TOKEN': … } }), and its middleware checks that header (Laravel CSRF protection). That one line has a cost. Step through the enquiry and the exchange rates of the shop page, first with $.ajaxSetup(), then with a prefilter:

Interactive · message by message A CSRF token in every request, and what it does to another origin Step through the enquiry and the exchange rates, first with $.ajaxSetup(), then with a prefilter.
A CSRF token in every request, and what it does to another origin, step 6 of 6. Messages so far: The shop page to The shop's server: POST /srv/enquiry; The shop page to The shop's server: X-CSRF-TOKEN: tok-5d1c; The shop page to Currency API: OPTIONS /rates; The shop page to The shop's server: POST with the token; The shop page to Currency API: GET /rates.The shop pageshop.exampleThe shop's serveranswered 201Currency APIanswered 200POST /srv/enquiryX-CSRF-TOKEN: tok-5d1cOPTIONS /ratesPOST with the tokenGET /rates

The drawing shows the whole scene; its 6 steps are listed below.

Every step as text
  1. No token. The enquiry goes out with X-Requested-With, which jQuery adds to every same-origin request, but without the CSRF token. The server refuses it as Laravel does: 419 Page Expired.
  2. $.ajaxSetup() adds it. $.ajaxSetup({ headers: { "X-CSRF-TOKEN": token } }) adds the header to every request from now on. The same enquiry carries it, and the server answers 201 Created.
  3. The rates ask first. The rates come from another origin, and the extra header makes the browser ask first: an OPTIONS request names X-CSRF-TOKEN in Access-Control-Request-Headers.
  4. The API says no. The API allows only X-Requested-With, so its answer has no CORS headers. The GET is never sent, and the request fails with status 0: the exchange rates disappear from the page.
  5. Same origin only. A prefilter adds the token only when options.crossDomain is false. The enquiry still carries it and gets 201, and the shop page never hands its token to another site.
  6. A plain GET. The rates request now has no custom header, not even X-Requested-With, which jQuery leaves off cross-origin requests. No OPTIONS is needed, and the API answers 200 with Access-Control-Allow-Origin.

The two pages make the same calls; the second adds the token only to requests for its own origin: The scheme, host and port of an address, such as https://shop.example. A request to any other origin is cross-origin, and the browser applies the CORS rules to it.:

The token pages: token-setup.js and token-prefilter.js

shop/token-setup.js

// The enquiry first goes without the CSRF token, then with the token added by $.ajaxSetup(), the way Laravel's
// documentation shows. The page also loads exchange rates from another origin, api.example.in.
const send = () => $.post('/srv/enquiry', { name: 'Asha K' });
const rates = () => $.getJSON('https://api.example.in/rates');
const outcome = async (label, request) => {
  try {
    console.log(`${label}: ${JSON.stringify(await request())}`);
  } catch (jqXHR) {
    console.log(`${label} failed: status ${jqXHR.status}`);
  }
};

(async () => {
  await outcome('enquiry without the token', send);
  const token = $('meta[name="csrf-token"]').attr('content');
  $.ajaxSetup({ headers: { 'X-CSRF-TOKEN': token } });
  await outcome('enquiry with the token', send);
  await outcome('rates', rates);
})();

shop/token-prefilter.js

// The same calls, with the token added by a prefilter only to requests for the shop's own origin.
const token = $('meta[name="csrf-token"]').attr('content');
$.ajaxPrefilter((options, originalOptions, jqXHR) => {
  if (!options.crossDomain) jqXHR.setRequestHeader('X-CSRF-TOKEN', token);
});
const outcome = async (label, request) => {
  try {
    console.log(`${label}: ${JSON.stringify(await request())}`);
  } catch (jqXHR) {
    console.log(`${label} failed: status ${jqXHR.status}`);
  }
};

(async () => {
  await outcome('enquiry', () => $.post('/srv/enquiry', { name: 'Asha K' }));
  await outcome('rates', () => $.getJSON('https://api.example.in/rates'));
})();

shop/token-setup.html

<!doctype html>
<html lang="en">
<head>
  <meta charset="utf-8">
  <meta name="csrf-token" content="tok-5d1c">
  <title>Enquiry with a CSRF token</title>
</head>
<body>
  <script src="https://code.jquery.com/jquery-4.0.0.min.js" integrity="sha256-OaVG6prZf4v69dPg6PhVattBXkcOWQB62pdZ3ORyrao=" crossorigin="anonymous"></script>
  <script src="token-setup.js"></script>
</body>
</html>
With $.ajaxSetup(): what the page logged and the server received JavaScript · token-setup.mjs
// Prints what shop/token-setup.js logged in jsdom and what the test server received (recorded.mjs).
import { show } from './show.mjs';

show('shop/token-setup.html');

Output

server: POST /srv/enquiry from XMLHttpRequest, answered 419
  Content-Type: application/x-www-form-urlencoded; charset=UTF-8
  X-Requested-With: XMLHttpRequest
  body: name=Asha+K
enquiry without the token failed: status 419
server: POST /srv/enquiry from XMLHttpRequest, answered 201
  Content-Type: application/x-www-form-urlencoded; charset=UTF-8
  X-Requested-With: XMLHttpRequest
  X-CSRF-TOKEN: tok-5d1c
  body: name=Asha+K
enquiry with the token: {"saved":true}
server: OPTIONS https://api.example.in/rates from XMLHttpRequest, answered 204
  Origin: https://shop.example
  Access-Control-Request-Method: GET
  Access-Control-Request-Headers: X-CSRF-TOKEN
rates failed: status 0
(jQuery 3.7.1 logged the same 16 lines)

Recorded with Node.js 24.21.0 on macOS 26 arm64. To run it yourself: mise exec node@24.21.0 -- node token-setup.mjs

With the prefilter JavaScript · token-prefilter.mjs
// Prints what shop/token-prefilter.js logged in jsdom and what the test server received (recorded.mjs).
import { show } from './show.mjs';

show('shop/token-prefilter.html');

Output

server: POST /srv/enquiry from XMLHttpRequest, answered 201
  Content-Type: application/x-www-form-urlencoded; charset=UTF-8
  X-Requested-With: XMLHttpRequest
  X-CSRF-TOKEN: tok-5d1c
  body: name=Asha+K
enquiry: {"saved":true}
server: GET https://api.example.in/rates from XMLHttpRequest, answered 200
  Origin: https://shop.example
rates: {"INR":1,"USD":0.0113}
(jQuery 3.7.1 logged the same 9 lines)

Recorded with Node.js 24.21.0 on macOS 26 arm64. To run it yourself: mise exec node@24.21.0 -- node token-prefilter.mjs

The test server checks the token on every enquiry, as Laravel 12’s ValidateCsrfToken middleware does (Laravel 12 CSRF protection), and answered 419 Page Expired, Laravel’s status for a token that does not match, until the token came with the request. With $.ajaxSetup(), the token header was also set on the call to api.example.in. A custom header turns a cross-origin request into one the browser must ask about first: an OPTIONS request, the preflight request: An OPTIONS request that a browser sends before a cross-origin request with a custom header or another method than GET, HEAD or POST, asking the server whether it allows it; the real request goes only if the answer allows it., listed X-CSRF-TOKEN in Access-Control-Request-Headers (Fetch Standard). The API allowed only X-Requested-With, so the GET was never sent and the rates failed with status 0. An API that allowed any header would have received the shop’s token instead, which is why OWASP’s cheat sheet warns against attaching the token to every cross-origin destination. The jQuery documentation advises against $.ajaxSetup() for another reason: its settings reach every later call, plugins included (jQuery.ajaxSetup()).

Laravel 13 checks the origin first

Laravel 13’s PreventRequestForgery middleware first reads the Sec-Fetch-Site header that browsers send over HTTPS, and lets a same-origin request through without a token; it checks the token only when that header does not settle the question, as in an older browser or over plain HTTP. Laravel 12 checks the token on every request that changes data. Code that sends the token works with both (Laravel 13 CSRF protection).

The prefilter runs before each request, after jQuery has worked out options.crossDomain, and adds the header only when it is false (jQuery.ajaxPrefilter()). The enquiry kept its token, and the rates went as a plain GET with only an Origin header.

X-Requested-With: A request header, X-Requested-With: XMLHttpRequest, that jQuery adds to its same-origin Ajax requests. Laravel, Symfony and Rails read it to tell an Ajax call from a page load; fetch() never sends it. follows the same rule inside jQuery: the XHR transport adds it to same-origin requests only, because “conditions for a preflight are akin to a jigsaw puzzle”, as the comment in its source puts it (src/ajax/xhr.js). Servers still read it. Laravel’s request()->ajax() calls Symfony’s isXmlHttpRequest(), which compares the header with XMLHttpRequest (Laravel, Symfony), and Rails’ request.xhr? reads it too (ActionDispatch::Request). Code that replaces $.ajax() has to keep sending it where such a check decides what the server returns.

Keep the token on your own origin

Add the CSRF token in a prefilter that checks options.crossDomain, or in the settings of each call to your own server, never with $.ajaxSetup() for every request. The token is a secret of your session; another site has no use for it except to misuse it.

Checkpoint

Check: CSRF tokens and X-Requested-With

  1. Question 1 of 2 After $.ajaxSetup({ headers: { "X-CSRF-TOKEN": token } }), the page's call to https://api.example.in/rates fails with status 0. Why?

    Choose one answer.

    Show the answer: Question 1 of 2

    Answer: The custom header made the browser send a preflight, and the API does not allow that header

    A header set with $.ajaxSetup() goes with every later request, to every origin. Add the token in a prefilter that checks options.crossDomain, so cross-origin requests stay simple.

  2. Question 2 of 2 Which requests does jQuery add X-Requested-With: XMLHttpRequest to by itself?

    Choose one answer.

    Show the answer: Question 2 of 2

    Answer: Same-origin requests only

    Servers such as Laravel's request()->ajax(), Symfony's isXmlHttpRequest() and Rails' xhr? read this header, so code that replaces $.ajax() may have to send it itself.

Practise: fix the enquiry request

The enquiry form has two bugs left from this lesson: its settings send JSON labelled as a form without the token, and its fail() callback shows a message only for a 404. Each step has starting code, a first hint and sample tests; the tests read plain objects shaped like the settings and the jqXHR, so they run without jQuery:

Code lab · 2 steps

Fix the enquiry form's request

Send the enquiry as JSON with its CSRF token, then show a message for every way the request can fail.

  1. Send the enquiry as JSON, with its CSRF token To do
  2. Show a message for every way the request can fail To do

Exercise · Easy · JavaScript

Send the enquiry as JSON, with its CSRF token

The enquiry form builds the settings it passes to $.ajax() in one function, enquirySettings(enquiry, token). Its PHP endpoint reads JSON from php://input and checks the CSRF token, and today it refuses every enquiry: the body is JSON text labelled as a form, and the token never leaves the page. Fix the function in enquiry.mjs so that the settings it returns:

  • send a POST to /srv/enquiry and ask for a JSON answer (dataType: 'json');
  • send the enquiry as JSON text with the Content-Type application/json;
  • carry the token in an X-CSRF-TOKEN header, and no other header.

The function must leave the enquiry it is given unchanged. The tests read the settings object, so they run without jQuery and without a browser.

Starter code · enquiry.mjs

/**
 * The $.ajax() settings that send an enquiry to the shop's PHP endpoint, which reads JSON and checks the CSRF token.
 * @param {{ name: string, services: string[], budget: number }} enquiry @param {string} token
 */
export function enquirySettings(enquiry, token) {
  return {
    url: '/srv/enquiry',
    method: 'POST',
    data: JSON.stringify(enquiry),
    dataType: 'json',
  };
}
The sample tests · enquiry.test.mjs
import { test, assert } from 'mysmartcopilot:test';
import { enquirySettings } from './enquiry.mjs';

const enquiry = { name: 'Asha K', services: ['web', 'seo'], budget: 50000 };

test('posts to /srv/enquiry and asks for a JSON answer', () => {
  const s = enquirySettings(enquiry, 'tok-5d1c');
  assert.equal(s.url, '/srv/enquiry');
  assert.equal(String(s.method ?? s.type).toUpperCase(), 'POST');
  assert.equal(s.dataType, 'json');
});

test('sends the enquiry as JSON text with the JSON Content-Type', () => {
  const s = enquirySettings(enquiry, 'tok-5d1c');
  assert.equal(s.contentType, 'application/json');
  assert.equal(typeof s.data, 'string');
  assert.deepEqual(JSON.parse(s.data), enquiry);
});

test('carries the token in X-CSRF-TOKEN and in no other header', () => {
  assert.deepEqual(enquirySettings(enquiry, 'tok-5d1c').headers, { 'X-CSRF-TOKEN': 'tok-5d1c' });
  assert.deepEqual(enquirySettings(enquiry, 'tok-9a0e').headers, { 'X-CSRF-TOKEN': 'tok-9a0e' });
});

test('leaves the enquiry it is given unchanged', () => {
  const before = JSON.stringify(enquiry);
  enquirySettings(enquiry, 'tok-5d1c');
  assert.equal(JSON.stringify(enquiry), before);
});
A hint

data: JSON.stringify(enquiry) already makes the body JSON text, but jQuery labels a string body with its default contentType, the form type. Set contentType yourself, and add a headers object with the one header.

The sample tests run on this device, in your browser (QuickJS): nothing is sent to mysmartcopilot.com. The first run downloads JavaScript (about 0.6 MB), which is kept for the next runs. A check in your browser is feedback for you, not proof that the code is right for every input.

Exercise · Medium · JavaScript

Show a message for every way the request can fail

The enquiry form calls failMessage(jqXHR, textStatus) in its fail() callback and shows the text it returns under the form. Today it returns a message only for a 404, so most failures show nothing at all. Write it in message.mjs, checking the cases in this order:

| The request | Return | | --- | --- | | textStatus is 'abort' (the page cancelled it) | '' | | textStatus is 'timeout' | 'The server took too long to answer. Please try again.' | | textStatus is 'parsererror' | 'The server sent an answer the page cannot read.' | | jqXHR.status is 0 | 'No connection to the server. Check your network and try again.' | | jqXHR.status is 419 | 'Your session has expired. Reload the page and send the form again.' | | jqXHR.status is 422 | the first message in jqXHR.responseJSON.errors, or 'Please check the form and send it again.' when there is none | | anything else | ` Something went wrong (status ${jqXHR.status}). Please try again. ` |

A 422 answer has Laravel's validation shape, { message, errors: { field: [messages] } }, and responseJSON is missing when the body was not JSON. The tests pass objects shaped like a jqXHR, without jQuery.

Starter code · message.mjs

/**
 * The text the enquiry form shows when a request fails, from the jqXHR and the textStatus that fail() receives.
 * @param {{ status: number, responseJSON?: { errors?: Record<string, string[]> } }} jqXHR @param {string} textStatus
 * @returns {string}
 */
export function failMessage(jqXHR, textStatus) {
  if (jqXHR.status === 404) return 'Not found.';
}
The sample tests · message.test.mjs
import { test, assert } from 'mysmartcopilot:test';
import { failMessage } from './message.mjs';

test('says nothing for a request the page cancelled itself', () => {
  assert.equal(failMessage({ status: 0 }, 'abort'), '');
});

test('tells a timeout from a lost connection, though both have status 0', () => {
  assert.equal(failMessage({ status: 0 }, 'timeout'), 'The server took too long to answer. Please try again.');
  assert.equal(failMessage({ status: 0 }, 'error'), 'No connection to the server. Check your network and try again.');
});

test('reports an answer that is not JSON, though its status is 200', () => {
  assert.equal(failMessage({ status: 200 }, 'parsererror'), 'The server sent an answer the page cannot read.');
});

test('asks for a reload when the CSRF token has expired', () => {
  assert.equal(failMessage({ status: 419 }, 'error'), 'Your session has expired. Reload the page and send the form again.');
});

test('shows the first validation message of a 422, or a general one', () => {
  const invalid = { status: 422, responseJSON: { message: 'Invalid.', errors: { email: ['The email field must be a valid email address.'], name: ['Too short.'] } } };
  assert.equal(failMessage(invalid, 'error'), 'The email field must be a valid email address.');
  assert.equal(failMessage({ status: 422 }, 'error'), 'Please check the form and send it again.');
  assert.equal(failMessage({ status: 422, responseJSON: { errors: {} } }, 'error'), 'Please check the form and send it again.');
});

test('names the status of any other failure', () => {
  assert.equal(failMessage({ status: 500 }, 'error'), 'Something went wrong (status 500). Please try again.');
  assert.equal(failMessage({ status: 404 }, 'error'), 'Something went wrong (status 404). Please try again.');
});
A hint

Test textStatus before jqXHR.status: a timeout and an abort both have status 0, and a parse error has status 200. For a 422, Object.values(errors) gives the lists of messages, field by field.

The sample tests run on this device, in your browser (QuickJS): nothing is sent to mysmartcopilot.com. The first run downloads JavaScript (about 0.6 MB), which is kept for the next runs. A check in your browser is feedback for you, not proof that the code is right for every input.

HTTP Status Codes Reference Look up any status your server sends, such as 419 or 422, before you write the message for it.

Then work through the lesson’s questions. They come one by one, starting from the idea you know least, and each answer you pick explains itself:

Practice that adapts to you

Practise this lesson

9 questions about 4 ideas of this lesson. Each answer is explained at once, and what you get right is kept in this browser.

  1. Question 1 of 9 What method and body does $.post("/srv/echo", { sku: "K-7310", qty: 2 }) send?

    Choose one answer.

    Show the answer: Question 1 of 9

    Answer: POST, with the form body sku=K-7310&qty=2

    The shorthand only fills in the method; the data follows the rules of $.ajax(): an object becomes a form body.

  2. Question 2 of 9 $.get("/srv/stock", { sku: "K-7310" }, fn) gives no dataType, and the answer has Content-Type application/json. What does fn receive first?

    Choose one answer.

    Show the answer: Question 2 of 9

    Answer: A parsed object

    The intelligent guess of $.ajax() follows the answer's Content-Type. Set dataType yourself when the code depends on what it receives.

  3. Question 3 of 9 How does a space in a value appear in the query string of $.get() and in the body of $.post() on jQuery 4?

    Choose one answer.

    Show the answer: Question 3 of 9

    Answer: %20 in the query string, + in the form body

    Both mean a space to the server. The difference matters only to code that compares raw strings, such as a signature or a test that checks the URL.

  4. Question 4 of 9 A page on jQuery 4 posts a FormData object with a file, using $.ajax({ method: "POST", data: form }). Which settings does it still need?

    Choose one answer.

    Show the answer: Question 4 of 9

    Answer: None: jQuery 4 sends FormData as multipart/form-data with the defaults

    jQuery 4 leaves FormData and other binary data unencoded and lets the browser set the Content-Type with its boundary.

  5. Question 5 of 9 A POST with dataType "json" gets 204 No Content. What happens?

    Choose one answer.

    Show the answer: Question 5 of 9

    Answer: done() runs with textStatus "nocontent" and data undefined

    For a write that returns nothing, 204 is the answer that jQuery treats as a success without a body.

  6. Question 6 of 9 A page has $(document).on("ajaxError", logIt). Which failed request does logIt not see?

    Choose one answer.

    Show the answer: Question 6 of 9

    Answer: One sent with global: false

    Global handlers on document see every request whose global setting is true, the default. Attach them with .on(): the .ajaxError() shorthand was deprecated in jQuery 3.5.

  7. Question 7 of 9 A fail() callback tests if (jqXHR.status === 0) first and shows "No connection". Which failures get the wrong message?

    Choose one answer.

    Show the answer: Question 7 of 9

    Answer: Timeouts and aborts, which also have status 0

    Order the tests from the most specific: textStatus "abort", "timeout" and "parsererror", then the status codes, then a message that names the status.

  8. Question 8 of 9 Which code adds a CSRF token to the shop's own requests and to no other?

    Choose one answer.

    Show the answer: Question 8 of 9

    Answer: $.ajaxPrefilter((o, orig, xhr) => { if (!o.crossDomain) xhr.setRequestHeader("X-CSRF-TOKEN", token); })

    OWASP recommends sending the token in a custom header from JavaScript and checking it on the server; keep that header on your own origin.

  9. Question 9 of 9 Which of these server checks read the X-Requested-With header?

    Choose every answer that is right.

    Show the answer: Question 9 of 9

    Answer:

    • Laravel's request()->ajax()
    • Rails' request.xhr?
    • Symfony's Request::isXmlHttpRequest()

    The header tells these frameworks that a request came from script, and some return JSON instead of a redirect when they see it. jQuery sends it to its own origin; fetch() never does.

Interview questions

Warm-up, for freshers and 2–5 years: how do $.get(), $.post() and $.ajax() differ?

$.ajax() is the one function that sends every request; $.get() and $.post() call it with the method filled in and take the URL, the data, a success callback and a dataType as arguments. They return the same jqXHR, so .done(), .fail() and await work on all three. Use $.ajax() when a call needs a setting the shorthands cannot pass, such as contentType, headers, timeout or processData; use the shorthands for plain GETs and form posts. $.getJSON() is a GET that asks for JSON, and .load() puts the HTML it fetches into the page.

Two harder questions from this module’s bank start from this lesson: an endpoint whose $_POST arrives empty, and a CSRF header that broke a call to another site. Try each one before you read its first step:

Model answers open with any pass: ₹79 for 1 week ($2.99), one payment that never renews.

  1. Fresher to senior

    After a refactor, a PHP endpoint that saved enquiries finds $_POST['name'] missing, although the browser shows the request answered with 200. The new code sends the enquiry with $.ajax(). How do you find the cause, and how do you fix it without breaking the endpoint's other callers?

    Model answer, Pro. Model answers open with any pass; prices are in the notice on this page. First step and follow-ups

  2. Mid to senior

    A team adds $.ajaxSetup({ headers: { 'X-CSRF-TOKEN': token } }) so that every form post carries Laravel's CSRF token. The next day a currency widget, which calls another company's API, shows no rates, and the console reports a CORS error. Explain what happened and how you would fix it.

    Model answer, Pro. First step and follow-ups

Key takeaways

  • $.get(), $.post(), $.getJSON() and .load() all call $.ajax() with settings filled in, and .load() without a selector runs the scripts of the HTML it inserts.
  • An object is sent as a form body with brackets and + for spaces, a string is sent as it is under the default form Content-Type, and JSON needs contentType: 'application/json', or PHP’s $_POST gets nothing it can use.
  • fail() runs for every status outside 200–299 and 304, for a body that does not parse, for a timeout and for an abort; a 204 succeeds with no data.
  • Read textStatus before the status: timeouts, aborts and lost connections all have status 0.
  • Send a CSRF token only to your own origin, from a prefilter or the call’s settings: with $.ajaxSetup() it reaches other origins, and its custom header forces a preflight there.

Terms in this lesson

CSRF token
A secret value that a server puts in its own pages and checks on every request that changes data. A request forged by another site cannot know it, so the server refuses that request.
jqXHR
The object that $.ajax() and its shorthands return: a promise of the request's result that also holds the status, the response text and the headers, with abort() to cancel the request.
origin
The scheme, host and port of an address, such as https://shop.example. A request to any other origin is cross-origin, and the browser applies the CORS rules to it.
preflight request
An OPTIONS request that a browser sends before a cross-origin request with a custom header or another method than GET, HEAD or POST, asking the server whether it allows it; the real request goes only if the answer allows it.
X-Requested-With
A request header, X-Requested-With: XMLHttpRequest, that jQuery adds to its same-origin Ajax requests. Laravel, Symfony and Rails read it to tell an Ajax call from a page load; fetch() never sends it.

References

Related tools

Report a problem with this lesson

Quick answers and tool search

Type to search tools or to get a quick answer, for example 18% of 2500. Use the up and down arrow keys to move through the results, Enter to choose, and Escape to close.