GithubHelp home page GithubHelp logo

Comments (40)

arvgta avatar arvgta commented on June 17, 2024

Hi and thanks for the praise and bringing up this issue and the description of what you're experiencing.
You're right, that the Ajaxify call itself can cause unwanted recursion, but the call you're using seems to be alright:

<script>
    let ajaxify = new Ajaxify();
</script>

I don't think that's the issue, because the syntax seems to be alright.

Thanks for the picture of your console.
It's rather obvious, that your system is somehow initiating an attempt to reload the whole (minified) Ajaxify library on each of such POSTs.
The Ajaxify plugin should never be reloaded at all.

Would you have a link to your site? I would be more than happy to try and help you debug this...

from ajaxify.

misog avatar misog commented on June 17, 2024

Hi, the alert is run only once and no duplicate init logs are created so it looks like the main problem is with AJAX response handling.

        <script>
            let ajaxify = new Ajaxify({
                verbosity: 100,
                canonical: true,
            });
            alert('once')
        </script>

Here are some server responses (Laravel):

return response('<p>test</p>', 200); // this works (DOM not modified)
return response('<p>test</p>', 201); // this works (DOM not modified)
return response('<p>test</p>', 299); // this works (DOM not modified)
return response('<p>test</p>', 300); // this loops requests from ajaxify
return response('<p>test</p>', 399); // this loops requests from ajaxify

So the error occurs when status code is not in 200 - 299 range. The site is not online yet.

from ajaxify.

arvgta avatar arvgta commented on June 17, 2024

Hi again.

The alert() is called only once, because any whole inline script containing the string:

  • "new Ajaxify("

...is ignored completely.
That's to say, that if Ajaxify is called from an inline script, it should only contain the Ajaxify call - no more, no less.

Really, I think the problem is that the POST somehow attempts to reload the whole library, which causes a sort of "recursion", because the whole logic kicks in by mistake again...

Yes, I appreciate, if your site is not online yet, but please take into account as well that offline sites may behave differently...

from ajaxify.

misog avatar misog commented on June 17, 2024

Hi, I created this test server script, can be named as whatever, ex. test.php:

<?php
    if ($_SERVER['REQUEST_METHOD'] === 'POST') {
        http_response_code(200); // 200, 404
        echo "<p>test</p>";
        exit;
    }
?>
<html>
<head>
    <script src="https://cdn.jsdelivr.net/gh/arvgta/[email protected]/ajaxify.min.js"></script>
    <script>
        let ajaxify = new Ajaxify({
            verbosity: 100,
        });
        console.log('init')
    </script>
</head>
<body>
    <form method="POST" action="">
        <button>Test</button>
    </form>
</body>
</html>

You are right, when http_response_code(200) and clicking at the button then it sends more and more requests (1, 2, 4, 8, ...) at each click: (console cleared after each click):
Screenshot from 2022-08-08 18-06-53
Screenshot from 2022-08-08 18-07-20
Screenshot from 2022-08-08 18-07-28

from ajaxify.

arvgta avatar arvgta commented on June 17, 2024

Alright, I really can't tell from the information so far.
Please remember when debugging, that the file

  • ajaxify.min.js

should only be loaded once on inital page load and never again after that.
So all you really have to examine from my point of view is, what is causing it to be reloaded in the first place...
Once reloaded, it is not foreseeable, what will happen next...


EDIT: I only saw your PHP script now - will have a look at it!

Regarding your PHP test script - why are you using:

  • action=""

within here:

<form method="POST" action="">

?

from ajaxify.

misog avatar misog commented on June 17, 2024

Yes, empty action will make it use the current url. Here it is explicitly set:

<?php
    if ($_SERVER['REQUEST_METHOD'] === 'POST') {
        http_response_code(200); // 200, 404
        echo "<p>test</p>";
        exit;
    }
?>
<html>
<head>
    <script src="https://cdn.jsdelivr.net/gh/arvgta/[email protected]/ajaxify.min.js"></script>
    <script>
        let ajaxify = new Ajaxify({
            verbosity: 100,
        });
    </script>
</head>
<body>
    <form method="POST" action="/test.php">
        <button>Test</button>
    </form>
</body>
</html>

No change.

from ajaxify.

arvgta avatar arvgta commented on June 17, 2024

Exactly. So that was your intention!

What is happening now?

from ajaxify.

misog avatar misog commented on June 17, 2024

Exactly the same. After each click, more requests are send at once. This is one issue. Another issue is that it tries to load POST request with GET.

from ajaxify.

misog avatar misog commented on June 17, 2024

Here I split it to two php scripts:
test.php

<html>
<head>
    <script src="https://cdn.jsdelivr.net/gh/arvgta/[email protected]/ajaxify.min.js"></script>
    <script>
        let ajaxify = new Ajaxify({
            verbosity: 100,
        });
    </script>
</head>
<body>
    <form method="POST" action="/postonly.php">
        <button>Test</button>
    </form>
</body>
</html>

postonly.php

<?php
    if ($_SERVER['REQUEST_METHOD'] === 'POST') {
        http_response_code(404); // 200, 404
        echo '<!DOCTYPE html>
        <html lang="en">
        <head>
            <meta charset="UTF-8">
            <meta http-equiv="X-UA-Compatible" content="IE=edge">
            <meta name="viewport" content="width=device-width, initial-scale=1.0">
            <title>Document</title>
        </head>
        <body>
            error message
        </body>
        </html>';
        exit;
    }
?>

The problem is that ajaxify tries to load postonly.php with GET:
Screenshot from 2022-08-08 18-43-46

from ajaxify.

arvgta avatar arvgta commented on June 17, 2024

I agree with you that this is weird.
Maybe not the most straight forward use-case but still weird...

Here is the current code within Ajaxify for form handling:

class Frms { constructor() {
	let fm = 0, divs = 0;

	this.a = function (o, p) {
		if (!Ay.s.forms || !o) return; //ensure data

		if(o === "d") divs = p; //set divs variable
		if(o === "a") divs.forEach(div => { //iterate through divs
		Array.prototype.filter.call(qa(Ay.s.forms, div), function(e) { //filter forms
			let c = e.getAttribute("action");
			return(Ay.internal(c && c.length > 0 ? c : Ay.currentURL)); //ensure "action"
		}).forEach(frm => { //iterate through forms
		frm.addEventListener("submit", q => { //create event listener
			fm = q.target; // fetch target

			p = _k(); //Serialise data
			var g = "get", //assume GET
			m = fm.getAttribute("method"); //fetch method attribute
			if (m.length > 0 && m.toLowerCase() == "post") g = "post"; //Override with "post"

			var h, a = fm.getAttribute("action"); //fetch action attribute
			if (a && a.length > 0) h = a; //found -> store
			else h = Ay.currentURL; //not found -> select current URL

			Ay.Rq("v", q); //validate request

			if (g == "get") h = _b(h, p); //GET -> copy URL parameters
			else {
				Ay.Rq("is", true); //set is POST in request data
				Ay.Rq("d", p); //save data in request data
			}

			Ay.trigger("submit", h); //raise pronto.submit event
			Ay.pronto(0, { href: h }); //programmatically change page

			q.preventDefault(); //prevent default form action
			return(false); //success -> disable default behaviour
		});
		});
	});
	};
let _k = () => {
		let o = new FormData(fm), n = qs("input[name][type=submit]", fm);

		if (n) o.append(n.getAttribute("name"), n.value);
		return o;
	},
	_b = (m, n) => {
		let s = "";
		if (m.iO("?")) m = m.substring(0, m.iO("?"));
		
		for (var [k, v] of n.entries()) s += `${k}=${encodeURIComponent(v)}&`;
		return `${m}?${s.slice(0,-1)}`;
	}
}}

from ajaxify.

misog avatar misog commented on June 17, 2024

I think POST with status code other than 200-299 is important usecase. If anything goes wrong on the server then response text should be displayed and JS library should not try to send GET request again. Error codes such as 404 and 500 are used by Laravel and other frameworks with custom error pages.

In another pjax library https://github.com/MoOx/pjax I handled response in such a way that it displayed error response immediately because it allows to override function:

    pjax.handleResponse = function(responseText, request, href, options) {
        if (responseText && responseText.match("<html")) {
            pjax._handleResponse(responseText, request, href, options);
        } else {
            window.scrollTo(0, 0);
            document.write(responseText || request.responseText);
        }
    }

Weird thing is that this is similar to another issue I created in yet another library: PaperStrike/Pjax#342

from ajaxify.

arvgta avatar arvgta commented on June 17, 2024

Thanks for your sharp thinking - much appreciated!

I'm trying to have a look the last issue in PJAX you mentioned.

What does your gut feeling say is the problem in Ajaxify?

from ajaxify.

arvgta avatar arvgta commented on June 17, 2024

Yes, I can confirm that Ajaxify tries to jump to the URL directly in case of failing of any AJAX request.
Here is the current salient code pertaining to any AJAX request:

	_lAjax = (hin, pre) => { 
		var ispost = Ay.Rq("is"); 
		if (pre) rt="p"; else rt="c"; 

		ac = new AbortController(); // set abort controller
		rc++; // set active request counter
		fetch(hin, {
			method: ((ispost) ? "POST" : "GET"),
			cache: "default",
			mode: "same-origin",
			headers: {"X-Requested-With": "XMLHttpRequest"},
			body: (ispost) ? Ay.Rq("d") : null,
			signal: ac.signal
		}).then(r => {
			if (!r.ok || !_isHtml(r)) {
				if (!pre) {location.href = hin; _cl(); Ay.pronto(0, Ay.currentURL);}
				return;
			}
			rsp = r; // store response
			return r.text();
		}).then(r => {
			_cl(1); // clear only plus variable
			if (!r) return; // ensure data
			rsp.responseText = r; // store response text
			
			return _cache(hin, r);
		}).catch(err => {
			if(err.name === "AbortError") return;
			try {
				Ay.trigger("error", err); 
				lg("Response text : " + err.message); 
				return _cache(hin, err.message, err);
			} catch (e) {}
		}).finally(() => rc--); // reset active request counter
	},

I suppose this bit especially:

if (!r.ok || !_isHtml(r)) {
	if (!pre) {location.href = hin; _cl(); Ay.pronto(0, Ay.currentURL);}
	return;
}

is not enough error handling?

from ajaxify.

arvgta avatar arvgta commented on June 17, 2024

Hold on, that code is problematic anyway, because there seem to be two "jumps" happening at once:

  • location.href = hin and
  • Ay.pronto(0, Ay.currentURL)

from ajaxify.

arvgta avatar arvgta commented on June 17, 2024

After having a closer look at your PJAX thread - I feel like simply leaving away the:

  • location.href = hin

...but am afraid that everything will fall over, when changing something that central?

from ajaxify.

arvgta avatar arvgta commented on June 17, 2024

Double checked against the last jQuery version of the plugin which had been reported to work really well:

It only does a:

  • location.href = hin

and nothing else.

I suspect the current code in the plain vanilla version does a "double jump"...

What would you advise me to prefer? ->

  • location.href = hin //breaks execution of Ajaxify

or

  • Ay.pronto(0, Ay.currentURL) //can maintain execution of Ajaxify

The former is safer because it worked in the old jQuery version.
The latter would be more exciting if it works.

from ajaxify.

misog avatar misog commented on June 17, 2024

There is var ispost = Ay.Rq("is"); maybe this flag could be used to differentiate GET requests from POSTS requests... Anyway, POST requests should really not be pre-fetched. Maybe lAjax could be duplicated to lAjaxPreFetch but I need to take better look at the code.

But I think it would be OK to create new config flag, like revisitPost or something.

from ajaxify.

arvgta avatar arvgta commented on June 17, 2024

Much appreciated!

  • Ay.Rq("is") //can be used to differentiate request types - yes

The question is, whether the bug does not simply apply to all failed AJAX requests (!)

The following code makes sure only non-prefetching requests trigger the code described previously:

if (!pre) ...


My gut feeling says, we need only one of these:

  • location.href = hin //breaks execution of Ajaxify but is nice and safe

or

  • Ay.pronto(0, Ay.currentURL) //can maintain execution of Ajaxify, not as safe but potentially powerful

POST requests should really not be pre-fetched.

I agree - that has been reported elsewhere to be a newly introduced bug in the plain vanilla version.
(I thought it might be due to the user setup but if you have the same finding...)


Am calling it a day, too, thanks very much for bringing this up!

from ajaxify.

misog avatar misog commented on June 17, 2024

You are welcome.

}).then(r => {
	if (!r.ok || !_isHtml(r)) {
		if (!pre) {location.href = hin; _cl(); Ay.pronto(0, Ay.currentURL);}
		return;
	}
	rsp = r; // store response
	return r.text();

location.href = hin; this will make browser to visit the fetched url. But then other two commands _cl(); Ay.pronto(0, Ay.currentURL);} will not execute (or their execution is probably meaningless because page is changed with a browser load, https://stackoverflow.com/questions/36398482/why-does-setting-window-location-href-not-stop-script-execution).

On the other hand, Ay.pronto(0, Ay.currentURL); will download the original URL (not the fetched one) and modify DOM.

Is there a reason that any of them should run if response is not OK or request is not HTML? Why to revisit any URL with GET?

Response.ok
The ok read-only property of the Response interface contains a Boolean stating whether the response was successful (status in the range 200-299) or not.
Sourse: https://developer.mozilla.org/en-US/docs/Web/API/Response/ok

And _isHtml checks headers.

I think that ajaxify should never repeat any request. Response is received and it should be handled and displayed if it is possible. So it should check if there is any text and just print it.

To make it backward compatible, maybe a new config option revisitOnError with true default value could be created:

	if (!r.ok || !_isHtml(r)) {
                if (revisitOnError) {
		    if (!pre) {location.href = hin; _cl(); Ay.pronto(0, Ay.currentURL);}
		    return;
                } else {
                    // handle displaying HTML (ex. 404 Not found styled page) or non HTML such as JSON, etc.
                    return;
                }
	}
        // handle OK case
	rsp = r; // store response
	return r.text();

I think it is OK to not store response in history because browser do not store failed responses too.

Also response in case of !r.ok must be handled in catch block or somewhere else because it is missing from the first then because ... javascript https://stackoverflow.com/questions/36225862/handle-a-500-response-with-the-fetch-api

from ajaxify.

arvgta avatar arvgta commented on June 17, 2024

First of all, many thanks for your findings, which I all agree with.

For a start in here:

then(r => {
	if (!r.ok || !_isHtml(r)) {
		if (!pre) {location.href = hin;/* _cl(); Ay.pronto(0, Ay.currentURL);*/}
				return;
	}
	rsp = r; // store response
	return r.text();
}

...I've commented out this code:

  • _cl(); Ay.pronto(0, Ay.currentURL);

...and committed the change to this file for the moment:

...and enabled this file and tested it against 4nf.org briefly - everything still seems to work...

Maybe you can see, whether the change has any effect on your system?

from ajaxify.

misog avatar misog commented on June 17, 2024

Now there is not infinite loop, so this one issue is solved.

But wrong server error is displayed in effect. It should display error 404 or 500 or whatever error the server wants to display. But it displays always 405 because ajaxify will load (location.href) the URL with GET, but that URL is POST only.
Screenshot from 2022-08-09 10-05-22

However the general problem is that ajaxify tries to repeat request in non-preload mode (so it is not expected to make requests without user invocation). This could generate false access logs on the server and cause other side effects. It is not such a big problem because GET is idempotent but wrong implemented server endpoints could be affected by this in a bad way. So I think revisits should be disabled or at least a config option such as revisitOnFailure could be implemented with default true value. In next major version it could be switched to default false if no complains from ajaxify users :)

from ajaxify.

misog avatar misog commented on June 17, 2024

Btw I commented out the entire block and now handlers are not multiplied so next clicks do not generate more and more requests:

}).then(r => {
	if (!r.ok || !_isHtml(r)) {
		// if (!pre) {location.href = hin; _cl(); Ay.pronto(0, Ay.currentURL);}
		return;
	}
	rsp = r; // store response
	return r.text();
}).then(r => {

Is there a reason why location.href = hin; is needed? In what case it is needed?

from ajaxify.

arvgta avatar arvgta commented on June 17, 2024

Wow, thanks very much for the test. I didn't necessarily think that the change would get rid of the "infinite loop" problem, which surely was bugging lots of users. I will commit this small change asap and create a new version...

The other bug, with the wrong method type and error value seems to be very similar to what you posted in the


However the general problem is that ajaxify tries to repeat request in non-preload mode...

I suppose you mean Ajaxify keeps on attempting the fallback we just debugged, even when:

  • prefetchoff: true

is specified by the user?

That's weird as well, but could we have look at that, when the other fix is committed properly to the repository?
I would like to make a new version(8.2.4) in the course of the day...

from ajaxify.

arvgta avatar arvgta commented on June 17, 2024

Is there a reason why location.href = hin; is needed? In what case it is needed?

Yes, if the MIME type is not detected successfully in:

  • _isHtml(r)

..other MIME types are "jumped" to. I see no other possibility just like the PJAX guys didn't either?

from ajaxify.

misog avatar misog commented on June 17, 2024

The other bug, with the wrong method type and error value seems to be very similar to what you posted in the

The bug is caused by ajaxify:

  1. Ajaxify fetch URL with POST
  2. Server returns error 404 (r.ok is false) and HTML output with error
  3. ajaxify loads the URL again with GET (location.href = hin) <-- here starts the bug
  4. Server returns 403 Wrong method.

So the problem is that ajaxify repeats the request with GET method. Is there any reason that ajaxify needs to repeat request after fetch was initiated?

I suppose you mean Ajaxify keeps on attempting the fallback we just debugged, even when:
prefetchoff: true

This happens with even with prefetchoff: true and this is not related to prefetch, because forms are not prefetched.

What is the pre variable?

from ajaxify.

arvgta avatar arvgta commented on June 17, 2024

What is the pre variable?

It is supposed to store, whether a prefetch is being handled


  1. ajaxify loads the URL again with GET (location.href = hin) <-- here starts the bug

Maybe it really does make sense to add a small check, whether the method is a POST

from ajaxify.

misog avatar misog commented on June 17, 2024

Yes, if the MIME type is not detected successfully in:

_isHtml(r)

..other MIME types are "jumped" to. I see no other possibility just like the PJAX guys didn't either?

But now the condition is not _isHtml(r), but the condition is (!r.ok || !_isHtml(r)) so "jump" happens even if MIME is HTML and request is not OK (ex. error 404).

Anyway why ajaxify tries to redirect browser to non-html MIME? JSON is application/json and ajaxify could display it. Maybe it is the wrong question what MIME the response has. The question is if there is something to be displayed. If MP3 file is returned then this should be handled by the browser, however JSON should he handled by document output.

I think this could be configured if to repeat request in certain MIME types, but I think it should try to just display everything if it is possible.

from ajaxify.

arvgta avatar arvgta commented on June 17, 2024

I agree:

  • (!r.ok || !_isHtml(r)) doesn't really cover all cases properly

...instead of the current isHTML(), we could introduce a function isDisplayable() instead?
But how can that be detected?

from ajaxify.

misog avatar misog commented on June 17, 2024

Yes, I was just typing this pseudocode!

if (shouldBeDisplayed(r)) {
    display(r);
}

It could be implemented by checking responseText propertiy or r.text() of the response object. However fetch is weird and in case of some status errors this property is available in catch, see the link:

https://stackoverflow.com/questions/36225862/handle-a-500-response-with-the-fetch-api

from ajaxify.

arvgta avatar arvgta commented on June 17, 2024

I don't think we need a new function:

  • display(r)

...because simply returning the text (which is fortunately default at the moment) ought to do the job?
On second thoughts, that will not work, because we are in the process of expecting the whole HTML of the page at that moment in time.

The trick with the responseText property of the response object sounds promising!

Yes, this here:

is going in the right direction, too...

from ajaxify.

misog avatar misog commented on June 17, 2024

And yes, check of method would be good. The main decision is if to handle response by ajaxify or repeat the action with standard browser load with location.href. Here is one idea:

// pseudocode
if (shouldBeHandled(r, method, ... other needed params to decide ...)) {
    // handle status codes, method, possible config mime options, ...
} else {
    // give it up to browser, possibly check configuration options
    if (repeatsEnabled) {
        location.href = url;
    } else {
        // call handler provided by developer
        config.requestFallBackHandler(response, requestOptions);
    }
}

from ajaxify.

arvgta avatar arvgta commented on June 17, 2024

Yes exactly, for all these new ideas to work, we would have to tweak Ajaxify to display non-HTML-like content in the case of error.
(As plain text or a really simple HTML error handling page)

from ajaxify.

arvgta avatar arvgta commented on June 17, 2024

Wow - alright! I think we both agree that that would entail quite a bit of additional Ajaxify code?
Is it alright for you, if I make the new version(8.2.4) as a fallback and genuine fix for the infinite loop problem first?

I'm not sure whether we can include an improvement of the dodgy condition:

  • (!r.ok || !_isHtml(r)) //doesn't really cover all cases properly

...as well (these two cases need to be handled separately for sure)

from ajaxify.

misog avatar misog commented on June 17, 2024

Yes, that would be very nice. Thank you.

from ajaxify.

arvgta avatar arvgta commented on June 17, 2024

What do you think? Should we attempt to include a better handling for this condition:

  • (!r.ok || !_isHtml(r)) //doesn't really cover all cases properly

?

from ajaxify.

misog avatar misog commented on June 17, 2024

I think in next version just infinite loop could be fixed and next next version two functions could exist:

  • _isHtmlHeader(r) as refactored _isHtml(r)
  • _hasTextContent(r) as actual check for content to be displayed

However _hasTextContent(r) needs to be called in catch block (there is the workaround needed to get response text) so it is more complicated than to modify that condition.

from ajaxify.

arvgta avatar arvgta commented on June 17, 2024

Thanks! Yes, I agree. I'll release the new version and we'll go from there...

from ajaxify.

arvgta avatar arvgta commented on June 17, 2024

I have created the new release.
Thanks very much!

from ajaxify.

misog avatar misog commented on June 17, 2024

That condition (!r.ok || !_isHtml(r)) just say that there is a hint that something is wrong. So it needs another handling. After reading this SO I think that two then blocks could be merged into one block which will resolve the text from response.text() promise: https://stackoverflow.com/questions/40408219/how-to-get-readable-error-response-from-javascript-fetch-api

}).then(r => {
	if (!r.ok || !_isHtml(r)) { // hint something is wrong
	      // store response in any case (location.href = hin would clear)
              rsp = r;
              r.text().then(text => { // resolve promise right there
                  if (!text) {
                      location.href = hin;
                      return;
                  }
	          _cl(1); // clear only plus variable
	          rsp.responseText = r; // store response text
	          return _cache(hin, r); // return does nothing probably
              });
	} else {
            rsp = r;
	    _cl(1); // clear only plus variable
	    rsp.responseText = r; // store response text
	    return _cache(hin, r);
        }
}).catch(err => {

But this is not tested and errors may not be handled.

from ajaxify.

arvgta avatar arvgta commented on June 17, 2024

Thanks, much appreciated!

  • !r.ok indicates that something is wrong - maybe a quick return with stopping execution but maybe returning the text nevertheless?
  • !isHtml() indicates that we're dealing with a "binary" - so to say - not necessarily a hard error. Or otherwise non-HTML plain text, which is a borderline case (this is probably the one you're interested in?)

I think these two cases should be handled separately in future (the first one first)

Thanks for the draft!

from ajaxify.

Related Issues (20)

Recommend Projects

  • React photo React

    A declarative, efficient, and flexible JavaScript library for building user interfaces.

  • Vue.js photo Vue.js

    🖖 Vue.js is a progressive, incrementally-adoptable JavaScript framework for building UI on the web.

  • Typescript photo Typescript

    TypeScript is a superset of JavaScript that compiles to clean JavaScript output.

  • TensorFlow photo TensorFlow

    An Open Source Machine Learning Framework for Everyone

  • Django photo Django

    The Web framework for perfectionists with deadlines.

  • D3 photo D3

    Bring data to life with SVG, Canvas and HTML. 📊📈🎉

Recommend Topics

  • javascript

    JavaScript (JS) is a lightweight interpreted programming language with first-class functions.

  • web

    Some thing interesting about web. New door for the world.

  • server

    A server is a program made to process requests and deliver data to clients.

  • Machine learning

    Machine learning is a way of modeling and interpreting data that allows a piece of software to respond intelligently.

  • Game

    Some thing interesting about game, make everyone happy.

Recommend Org

  • Facebook photo Facebook

    We are working to build community through open source technology. NB: members must have two-factor auth.

  • Microsoft photo Microsoft

    Open source projects and samples from Microsoft.

  • Google photo Google

    Google ❤️ Open Source for everyone.

  • D3 photo D3

    Data-Driven Documents codes.