6

Another I-did-not-sleep-enough question, I'm sure. I'm posting it as a sacrifice to the Elder God Murphy: as soon as I expose my moronity for all to see, I'm guaranteed to find by myself that answer that otherwise will elude me for hours (by way of further penance, I will then post the answer as well).

I have a HTML form that gets rendered as

<form method="post" id="mysearch" action="/search/?uid=1701">
    <input id="searchterm" type="text" name="query" />
</form>

The form can be submitted via jQuery $.POST with url of '/search' and data of { uid: '1701', query: $('#searchterm').val() } and it works.

If I press ENTER after entering something, and thus override the jQuery submission, the following happens:

  • a POST is issued to the server, as expected.
  • the Route::post('/search', function() {... does not get invoked.
  • a 301 Moved Permanently is returned
  • a GET with search parameters lost is issued to the URL specified by the Redirect
  • quite obviously, the search fails.

The 301 response looks like something by Laravel4, added explicitly:

HTTP/1.0 301 Moved Permanently
Date: Thu, 28 Nov 2013 14:05:29 GMT
Server: Apache
X-Powered-By: PHP/5.4.20
Cache-Control: no-cache
Location: http://development/search?uid=1701
Connection: close
Content-Type: text/html

<!DOCTYPE html>
<html>
<head>
    <meta http-equiv="Content-Type" content="text/html; charset=utf-8" />
    <meta http-equiv="refresh" content="1;url=http://development/search?uid=1701" />
    <title>Redirecting to http://development/search?uid=1701</title>
</head>
<body>
Redirecting to <a href="Redirecting to http://development/search?uid=1701">Redirecting to http://development/search?uid=1701</a>
</body>
</html>

This is not the same as this question, because there the redirect is expected and it's the answer to that which is undesired. Here it's the redirect itself that is generated for no reason that I can (for now) see.

I suspect that for some reason I'm triggering the "security redirect" described in this other answer, that is not triggered by jQuery (either because it puts everything in the POST while here I've one parameter in the URL and another in the POST, or because jQuery uses XHR).

I had thought it might be a CSRF defense, but that particular route isn't shielded. As a last resource, I'll CSRF-protect the route and add the token to the form, even if it looks a bit like voodoo to me. Something vaguely similar appears to be happening in Rails.

Workarounds

I've got not one, not two, but three workarounds that neatly sidestep the question of why is the above happening:

  • (most brutal) block the keyUp event in the form.
  • redirect the submit event of the form to jQuery
  • (most transparent) route the above event to $('#search-button').click()

...but I'd like to make without the button altogether (which I could do with jQuery) and without jQuery altogether. As well as understand what is happening here. I'm 99% certain I'm missing something obvious.

Debugging

I'm now going to grep -r "Redirecting to" * the whole framework source code (I expect to find something in Symfony/Components/HttpFoundation/ResponseRedirect) and doing step-by-step from there.

Community
  • 1
  • 1
LSerni
  • 55,617
  • 10
  • 65
  • 107
  • 1
    This permanent redirect is a result of the trailing slash in your form action url – peaceman Nov 28 '13 at 14:50
  • Should have guessed :-) -- if you post it as an answer, I'll be happy to upvote. And accept it too, if you also have an idea of *why* it should behave like this (I can't seem to come up with an use case). – LSerni Nov 28 '13 at 14:53
  • Some people like to have trailing slashes in their application urls and to be persistent with this preference, they ensure it with a permanent redirect. For example, trailing slashes are the default in django. – peaceman Nov 28 '13 at 14:59
  • I see... I still think it weird, but I'll accept that answer. I have indeed dabbled with Django, so maybe that's why the slash didn't come immediately to my attention. I feel a *bit* less stupid. Thanks :-) – LSerni Nov 28 '13 at 15:05

1 Answers1

18

TL;DR

ensure POST URLs do not end with a slash. -- but Laravel 4.1 users check the update below, first.

=======

When it works, it's not superstition - it's science :-(

as soon as I expose my moronity for all to see, I'm guaranteed to find by myself that answer that would otherwise elude me for hours

I will argue that Laravel4's HTML message could have been just a bit more informative.

The grep found as expected the origin of the redirect:

grep -r "Redirecting to" *
vendor/symfony/http-foundation/Symfony/Component/HttpFoundation/RedirectResponse.php:        <title>Redirecting to %1$s</title>
vendor/symfony/http-foundation/Symfony/Component/HttpFoundation/RedirectResponse.php:        Redirecting to <a href="%1$s">%1$s</a>.

at that point, a simple backtrace found the origin, very early in Laravel4 spin-up:

bootstrap/start.php:

...
| The first thing we will do is create a new Laravel application instance
| which serves as the "glue" for all the components of Laravel, and is
| the IoC container for the system binding all of the various parts.
|
*/

$app = new Illuminate\Foundation\Application;

$app->redirectIfTrailingSlash();

As soon as I saw the redirectIfTrailingSlash, I realized that the two URLs sent by the form and by jQuery were not the same:

... action="/search/?uid=1701">   <--- TRAILING SLASH AFTER 'search'

... url:   '/search',             <--- NO TRAILING SLASH
    data:  {
               uid  : 1701,
               query: $('#searchterm').val()
           },
...

Why this should happen I don't quite grok, but the solution is fiendishly simple:

remove the slash from the POST action field.

(and ensure .htaccess has no rule to consider the URL a "directory" and add back the slash - but if it had, jQuery would have failed too).

UPDATE

Apparently, the matter got reviewed in Laravel 4.1. The upgrade mentions

Removing Redirect Trailing Slash

In your bootstrap/start.php file, remove the call to $app->redirectIfTrailingSlash(). This method is no longer needed as this functionality is now handled by the .htaccess file included with the framework.

Next, replace your Apache .htaccess file with this new one that handles trailing slashes.

LSerni
  • 55,617
  • 10
  • 65
  • 107
  • 2
    upvoted, and I can't help it but have to post this link: https://twitter.com/taylorotwell/status/345664701805494272 – Rob Gordijn Nov 28 '13 at 16:30
  • LOL! Well, I don't have a preference and URLs are maybe better *without* trailing slashes. I'm cool either way. Still it's good to know where to put my hands if I ever have to change from the "correct behaviour" to the *other* correct behaviour :-) – LSerni Nov 28 '13 at 20:36
  • 1
    Strangely, I got this issue today in Laravel Framework 8.22.1 And fixed by removing trailing slash in this post :) – userDuR Oct 15 '21 at 09:01