WordPress / wordpress-playground

Run WordPress in the browser via WebAssembly PHP
https://w.org/playground/
GNU General Public License v2.0
1.64k stars 257 forks source link

Synchronous XHR request not supported, always yielding a 404 error #1384

Open key88sf opened 6 months ago

key88sf commented 6 months ago

It looks like any calls by plugins to admin-ajax.php result in a 404 Not Found error when running inside a playground instance.

Is there any way to enable AJAX requests in the blueprint JSON?

bgrgicak commented 6 months ago

Is there any way to enable AJAX requests in the blueprint JSON?

Would you be able to provide an example blueprint to help us better understand your issue?

key88sf commented 6 months ago

@bgrgicak Yes - so I was testing with our plugin using this Blueprint JSON below (plugin name redacted here):

{
  "$schema": "https://playground.wordpress.net/blueprint-schema.json",
  "landingPage": "/wp-admin/index.php",
  "preferredVersions": {
    "php": "8.3",
    "wp": "latest"
  },
  "phpExtensionBundles": [
    "kitchen-sink"
  ],
  "features": {
    "networking": true
  },
  "steps": [
    {
      "step": "login",
      "username": "admin",
      "password": "password"
    },
    {
      "step": "installPlugin",
      "pluginZipFile": {
        "resource": "wordpress.org/plugins",
        "slug": "my-plugin-slug-name"
      },
      "options": {
        "activate": true
      }
    }
  ]
}

Our plugin calls admin-ajax.php on various pages, for example from the media library edit page. But in the Playground instance, this is giving a 404:

URL: /wp-admin/post.php?post=5&action=edit

Chrome console: POST https://playground.wordpress.net/scope:0.9331797754628046/wp-admin/admin-ajax.php 404 (Not Found)

bgrgicak commented 6 months ago

Is there any way to enable AJAX requests in the blueprint JSON?

Ajax requests are enabled by default. You can see it by loading wp-admin in Playground and checking the network tab.

I assume that something else related to Playground is causing the issue for you.

If you can't share the plugin with us, you could debug it yourself by taking a look at the Playground logs (browser console, or click on View Logs in the upper right menu).

key88sf commented 5 months ago

This is really strange -- the URL is exactly the same between what the plugin is posting to and what the wp-admin page is posting to.

The only difference I see is in some of the network headers. There are additional security headers which are sent when the plugin javascript makes the request (via jQuery.ajax()):

Sec-Fetch-Dest: empty
Sec-Fetch-Mode: cors
Sec-Fetch-Site: same-origin

This is the JS code which makes the call:

      jQuery.ajax({
        type: 'post',
        dataType: 'json',
        async: false,
        data: {
          'action': 'alax_check_image',
          'security': wp_alax.security_check_image,
          'attachment_id': attachmentId,
        },
        url: wp_alax.ajax_url, // this is set to: admin_url( 'admin-ajax.php' )
        success: function (response) {
          status = response.status;
        }
      });

Does that help at all?

bgrgicak commented 5 months ago

I don't think it's the security headers. I tried adding them to a heartbeat request and it kept working.

The code you provided doesn't work for me because I don't have the alax_check_image endpoint. Would you be able to provide instructions on how to recreate this in Playground? I would love to help, but first I need to be able to recreate the issue.

To debug it further you could rewrite the request to use fetch and see if this resolves the issue. If yes, compare the two requests to see what happened.

Example:

fetch(
wp.src.match(/(^[^\/]*\/\/[^\/]*\/[^\/]*)\/.*/)[1] +
    "/wp-admin/admin-ajax.php",
  {
    headers: {
      accept: "application/json, text/javascript, */*; q=0.01",
      "content-type": "application/x-www-form-urlencoded; charset=UTF-8",
      "x-requested-with": "XMLHttpRequest",
        "Sec-Fetch-Dest": "empty",
        "Sec-Fetch-Mode": "cors",
        "Sec-Fetch-Site": "same-origin",
    },
    referrer:
      "https://playground.wordpress.net/scope:0.8893762696707332/wp-admin/edit-comments.php",
    referrerPolicy: "strict-origin-when-cross-origin",
    body: "interval=60&_nonce=1c0d4e2483&action=heartbeat&screen_id=edit-comments&has_focus=false",
    method: "POST",
    mode: "cors",
    credentials: "omit",
  }
)
  .then((response) => response.json())
  .then((data) => console.log(data.text()))
  .catch((error) => console.log(error));
key88sf commented 5 months ago

The code you provided doesn't work for me because I don't have the alax_check_image endpoint. Would you be able to provide instructions on how to recreate this in Playground? I would love to help, but first I need to be able to recreate the issue.

This isn't an endpoint, it's just a part of the JSON data being sent -- so not really important. We just use that to ensure the request is coming from Wordpress.

Is there any way to test changes to the plugin code in playground without actually committing new code to SVN?

bgrgicak commented 5 months ago

Is there any way to test changes to the plugin code in playground without actually committing new code to SVN?

Yes, you can use the device storage option inside Playground. Alternatively, you could use WP-NOW as a development environment. WP-NOW is slightly different from the web version, so in case your code works with WP-NOW, this would be a Playground web issue.

adamziel commented 5 months ago

Is there any way to test changes to the plugin code in playground without actually committing new code to SVN?

Or you could:

It would be handy to document different workflow options for plugin preview development. Let's link this issue with https://github.com/WordPress/wordpress-playground/issues/772 for the upcoming doc overhaul

key88sf commented 5 months ago

@bgrgicak I figured out what is causing the problem. It is the async: false setting in the jQuery.ajax() call.

If I set this to true, the POST to admin-ajax.php works fine (200 response). Setting to false results in a 404 Not Found error. I'm not sure why using synchronous results in a 404 though?

The jQuery docs (https://api.jquery.com/Jquery.ajax/) only say that if you make a synchronous request via cross-domain, it is not supported -- but the call works fine on "real" WP instances, just not in the Playground.

Any ideas?

bgrgicak commented 5 months ago

If I set this to true, the POST to admin-ajax.php works fine (200 response). Setting to false results in a 404 Not Found error. I'm not sure why using synchronous results in a 404 though?

Great find, I can confirm that async false triggers a 404.

Here is an example that returns a 404, but it returns 200 if async is true.

jQuery = import('https://code.jquery.com/jquery-3.7.1.min.js');

jQuery.ajax({
    url: wp.src.match(/(^[^\/]*\/\/[^\/]*\/[^\/]*)\/.*/)[1] + "/wp-admin/admin-ajax.php",
    type: 'post',
    headers: { "Accept-Encoding" : "gzip" },
    dataType: 'json',
    async: false,
    data: {
        action: 'heartbeat',
    },
}).done(function(data) {
    console.log(data);
});
bgrgicak commented 5 months ago

From looking at the ajax documentation this part stands out Note that synchronous requests may temporarily lock the browser, disabling any actions while the request is active.. I assume that when a sync request is sent, Playground is blocked from running and accepting the request.

@adamziel please correct me if I'm wrong.

@key88sf I would suggest that you to rewrite the code to be async. I'm not sure if Playground would be able to support sync requests, and in general they are a bad practice because they block the browser while running.

key88sf commented 5 months ago

@bgrgicak Cool we can try to rewrite as an async call. I'm sure there are other plugins out there which make sync requests so if possible to support this, that would be great!

adamziel commented 5 months ago

I assume that when a sync request is sent, Playground is blocked from running and accepting the request.

PHP runs in a worker, the only relevant part that would get blocked part is this event listener relaying messages between the service worker and PHP web worker: https://github.com/WordPress/wordpress-playground/blob/16293ba57cea96e0f7f3eca12b0e6aef8f3b2f8e/packages/php-wasm/web/src/lib/register-service-worker.ts#L47

The service worker used to talk directly to the web worker via a BroadcastChannel, but it wasn't working consistently across web browser and there were also issues with CORS/embedding Playground:

https://github.com/WordPress/wordpress-playground/blob/16293ba57cea96e0f7f3eca12b0e6aef8f3b2f8e/packages/php-wasm/web-service-worker/src/initialize-service-worker.ts#L137-L139

If this, or any other communication technique would work consistently these days, we could refactor that part.

Relevant article: https://web.dev/articles/two-way-communication-guide