0

I have been delving into AJAX within WordPress and have followed many tutorials on how to do so, but every single request returns a 400 error.

In my functions.php I have added the following:

Step 1 - Register and add scripts to WordPress

/**
 * Initialize the JavaScript we need for loading more
 *
 * @return void
 */
function ajax_load_more_init()
{
    // Register and enqueue the script we need for load more
    wp_register_script('ajax-load-more-script', get_template_directory_uri() . '/assets/scripts/ajax-load-more.js', array('jquery'));
    wp_enqueue_script('ajax-load-more-script');

    // Localize the script so we can access tthe variables in PHP
    wp_localize_script('ajax-load-more-script', 'ajax_load_more_object', array(
        'ajax_url' => admin_url('admin-ajax.php'),
    ));
}

Step 2 - Enqueue the actual script

/**
 * Add AJAX loader to scripts
 */
add_action('wp_enqueue_scripts', 'ajax_load_more_init');

Step 3 - Allow AJAX to be used on the frontend

// AJAX Hook for users
add_action('wp_ajax_ajax_load_more', 'ajax_load_more');
add_action('wp_ajax_nopriv_ajax_load_more', 'ajax_load_more');

Step 4 - A dead simple function for testing

/**
 * The backend PHP to actually load more posts
 *
 * @return void
 */
function ajax_load_more()
{
    echo "TESTING";

    wp_die();
}

In my actual AJAX script I have the following:

jQuery(document).ready(function ($) {

  // Initialize Isotope as $grid
  const $grid = $('#grid').isotope({
    itemSelector: '.grid-item',
    percentagePosition: true,
    animationEngine: 'best-available', //CSS3 if browser supports it, jQuery otherwise
    animationOptions: {
      duration: 800,
    },
    masonry: {
      columnWidth: '.grid-item',
      gutter: 30,
    },
  })


  var has_run = false;
  var init_offset = 0;

  // Hook into click event
  $('button.load-more-posts').click(function (e) {
    e.preventDefault();

    var button = $(this);
    var nonce = $(this).data("nonce");

    console.log('Nonce is: ' + nonce);

    // Disable the button
    button.prop("disabled", true);

    // Check the offset
    if (has_run == false) {
      button.data('offset', $(this).data("offset"));
      init_offset = $(this).data("offset");
    }

    console.log('Initial offset is: ' + init_offset);
    console.log('Initial offset is: ' + button.data('offset'));

    // Perform AJAX request
    $.ajax({
      type: 'POST',
      dataType: 'json',
      url: ajax_load_more_object.ajax_url,
      contentType: 'application/json; charset=utf-8',
      data: {
        action: 'ajax_load_more',
        security: nonce,
        init_offset: init_offset,
        offset: button.data('offset'),
      },
      beforeSend: function (xhr) {
        console.log('Loading more posts...')
        button.text('Loading');
      },
      success: function (response) {
        console.log(response);

        // Undo Button Disable
        button.prop("disabled", false);

        // Set Offset
        button.data("offset", offset + 10);

        // Script has run
        has_run = true;

        return false;
      },
      error: function (xhr, status, error) {
        console.log(xhr.responseText);
      }
    });
  });
});

As you can see in the $ajax call, the url is to admin-ajax and the action is the trailing part of wp_ajax_nopriv_ajax_load_more.

The nonce comes from the button on the front end like so:

<button class="load-more-posts" data-nonce="<?php echo wp_create_nonce('load_more_ajax'); ?>" data-offset="10">Load More Posts</button>

enter image description here

My working code

So, thanks to everyone's shared input I was able to come up with a solution that I'll detail below:

In functions.php (should really be in a plugin)

/**
 * Additional thumbnail sizes
 */
add_theme_support('post-thumbnails');
add_image_size('post-thumbnail-square', 300, 300, true);

/**
 * Initialize the JavaScript we need for loading more
 *
 * @return void
 */
function ajax_load_more_init()
{
    // Register and enqueue the script we need for load more
    wp_register_script('ajax-load-more-script', get_template_directory_uri() . '/assets/scripts/ajax-load-more.js', array('jquery'));
    wp_enqueue_script('ajax-load-more-script');

    // Localize the script so we can access tthe variables in PHP
    wp_localize_script('ajax-load-more-script', 'ajax_load_more_object', array(
        'ajax_url' => admin_url('admin-ajax.php'),
    ));
}

/**
 * Add AJAX loader to scripts
 */
add_action('wp_enqueue_scripts', 'ajax_load_more_init');

/**
 * Allow AJAX to be used on the front end
 */
add_action('wp_ajax_ajax_load_more_posts', 'ajax_load_more_posts_callback');
add_action('wp_ajax_nopriv_ajax_load_more_posts', 'ajax_load_more_posts_callback');

/**
 * The backend PHP to actually load more posts
 *
 * @return void
 */
function ajax_load_more_posts_callback()
{
    // First check the nonce, if it fails the function will break
    check_ajax_referer('ajax_load_more_posts', 'security');

    // Get the data we have from the load more button
    $offset = $_POST['offset'];
    $init_offset = $_POST['init_offset'];

    // Get posts with given offset
    if ($offset != null && absint($offset) && $init_offset != null && absint($init_offset)) {
        // Finally, we'll set the query arguments and instantiate WP_Query
        $args = array(
            'post_type' => 'post',
            'posts_per_page' =>  $init_offset,
            'offset' => $offset
        );

        $post_list = array();

        $query = new WP_Query($args);

        if ($query->have_posts()) :
            while ($query->have_posts()) : $query->the_post();

                //$categories = get_the_categories();

                $post_list[] = array(
                    'category' => get_the_category(),
                    'title' => get_the_title(),
                    'introduction' => get_field('introduction'),
                    'date' => get_the_date(),
                    'permalink' => get_permalink(),
                    'thumbnail' => get_the_post_thumbnail(),

                );
            endwhile;
        endif;

        echo json_encode($post_list);

        wp_die();
    }
}

In my ajax-loader script

jQuery(document).ready(function ($) {

  var has_run = false;
  var init_offset = 0;

  // Hook into click event
  $('button.load-more-posts').click(function (e) {

    var $grid = $('#grid').isotope({
      itemSelector: '.grid-item',
    });

    e.preventDefault();

    var button = $(this);
    var nonce = $(this).data("nonce");

    // Disable the button
    button.prop("disabled", true);

    // Check the offset
    if (has_run == false) {
      button.data('offset', $(this).data("offset"));
      init_offset = $(this).data("offset");
    }

    // Perform AJAX request
    $.ajax({
      type: 'POST',
      dataType: 'json',
      url: ajax_load_more_object.ajax_url,
      data: {
        action: 'ajax_load_more_posts',
        security: nonce,
        init_offset: init_offset,
        offset: button.data('offset'),
      },
      beforeSend: function (xhr) {
        console.log('Loading more posts...')
        button.text('Loading');
      },
      success: function (response) {
        console.log(response);
        button.text('Load more');

        // An array to store new items added via AJAX
        var new_items = [];

        // Run through JSON
        $.each(response, function (key, value) {

          var $new_item = $(`<div class="grid-item article-post-card ${value.category[0]['slug']}">
                                <a class="article-post-card__anchor" href=" ${value.permalink}" alt="${value.title}">
                                     <div class="article-post-card__featured-image-container">
                                        <div class="article-post-card__overlay"></div>
                                        <div class="article-post-card__featured-image">
                                            ${value.thumbnail}
                                        </div>

                                        <div class="article-post-card__category-label">
                                            ${value.category[0]['name']}
                                        </div>

                                    </div>

                                     <div class="article-post-card__content-wrapper">

                                        <div class="article-post-card__publish-date">
                                            <time class="updated" datetime="">${value.date}</time>
                                        </div>

                                        <div class="article-post-card__title">
                                            ${value.title}
                                        </div>

                                        <div class="article-post-card__excerpt">
                                          ${value.introduction}
                                        </div>

                                    </div>
                                </a>
                            </div>`);

          new_items.push($new_item[0]);
        });

        // Add the new items to the grid
        $grid
          .isotope('insert', new_items)
          .imagesLoaded().progress(function () {
            $grid.isotope('layout');
          });

        // Undo Button Disable
        button.prop("disabled", false);

        // Set Offset
        var offset = button.data("offset");
        button.data("offset", offset + 10);

        // Script has run
        has_run = true;

        return false;
      },
      error: function (xhr, status, error) {
        console.log("There was an error", error);
      }
    });
  });
});

If you guys could critique I think this would be useful for others too.

6
  • Can you check the PHP logs on your server? Does disabling your plugin or switching to another theme fix the problem, i.e. can you test if it's in your code, or something else?
    – Rup
    Commented Mar 12, 2019 at 12:20
  • I'll try, but does it look correct? Commented Mar 12, 2019 at 12:22
  • Check your request with dev tools under the network tab and check the params, action (value: ajax_load_more ) must be submitted correctly. You can also try to send a simple request with an external tool and debug it. On mac i use "Insomnia", a great tool.
    – endcoreCL
    Commented Mar 12, 2019 at 12:26
  • The request payload is: action=ajax_load_more_posts&security=25a70fd9b9&init_offset=10&offset=10 Commented Mar 12, 2019 at 12:32
  • Trying it using Insomnia also yields a response of 0 Commented Mar 12, 2019 at 12:47

2 Answers 2

5

I have not tested your code, but one problem I noticed is the contentType property in your $.ajax() call:

contentType: 'application/json; charset=utf-8',

because that way, (from the PHP side) the action (i.e. ajax_load_more) is not available in $_REQUEST['action'] which WordPress uses to determine the AJAX action being called, and when the action is not known, WordPress throws the error 400 Bad Request, which in your case translates to "unknown AJAX action".

You should just omit the contentType property, or don't set it to a JSON content type string.

Also, although you're just testing in ajax_load_more(), make sure to return a proper JSON response/string because your dataType is json.

I hope this helps you. :)

Additional Notes

From the jQuery.ajax() documentation:

  1. dataType (default: Intelligent Guess (xml, json, script, or html))

    The type of data that you're expecting back from the server. If none is specified, jQuery will try to infer it based on the MIME type of the response

  2. contentType (default: 'application/x-www-form-urlencoded; charset=UTF-8')

    When sending data to the server, use this content type. Default is "application/x-www-form-urlencoded; charset=UTF-8", which is fine for most cases. If you explicitly pass in a content-type to $.ajax(), then it is always sent to the server (even if no data is sent).

So dataType is the content type (MIME) of the content from the AJAX response, whereas contentType is the content type of the content you are sending to the PHP/server-side function (which is ajax_load_more() in your case).

UPDATE

So by using the wrong content type the headers become unreadable?

No it is not about the headers become unreadable - (request) headers are always readable in PHP. It's just that request payload (in JSON) is not supported by the WordPress AJAX - only the request body. But you can of course, send form field data/value as JSON - for example, myField: '{"foo":"bar","baz":1}'. Just don't set the content type header to JSON.

But if you really must send a JSON request payload instead of a standard URL-encoded form data (i.e. the request body), then you can append the AJAX action to the AJAX URL - e.g. /wp-admin/admin-ajax.php?action=ajax_load_more. That way, you wouldn't get the error 400; however, it's up to you to retrieve/parse the JSON request payload..

Request Payload (JSON)

jQuery.ajax({
  type: 'POST',
  url: '/wp-admin/admin-ajax.php?action=ajax_load_more',
  contentType: 'application/json; charset=utf-8',
  data: {
    action: 'ajax_load_more',
    foo: 'bar',
  }
});

Request Body (Form Data)

jQuery.ajax({
  type: 'POST',
  url: '/wp-admin/admin-ajax.php',
  data: {
    action: 'ajax_load_more',
    foo: 'bar',
  }
});

3
  • So by using the wrong content type the headers become unreadable? Commented Mar 12, 2019 at 12:54
  • I've just updated the answer. Hopefully it's more helpful now.. to you..
    – Sally CJ
    Commented Mar 12, 2019 at 14:56
  • I will share what I was able to get working. Commented Mar 12, 2019 at 23:35
-1

You haven't specify any response, functions that handle the ajax requests must return a json encoded array using wp_send_json _success() built-in function or other similar functions.

Exemple :

function ajax_load_more(){

    wp_send_json_success('testing...');

//Don t nee to use die()  wp_send_json success will send json and die

      } 

you can find out how these functions work in the wordpress codex, the php function can't echo anything, php send data to jquery and jquery then can echo data to html.

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Not the answer you're looking for? Browse other questions tagged or ask your own question.