Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Fordward renders 404 error when should not #11102

Closed
3 tasks done
maurociancio opened this issue Oct 7, 2018 · 22 comments
Closed
3 tasks done

Fordward renders 404 error when should not #11102

maurociancio opened this issue Oct 7, 2018 · 22 comments
Assignees
Milestone

Comments

@maurociancio
Copy link

I've created a sample project showing the problem: https://github.com/maurociancio/grails-3-forward-issue .

Given the following controller:

class TestController {

    def get() {
    }

    def post() {
      forward action: 'get'
    }
}

And the following UrlMappings:

class UrlMappings {

    static mappings = {
        name testMapping: '/test-mapping'(controller: 'test') {
          action = [GET: 'get', POST: 'post']
        }

        /*
        PLEASE NOTE THIS SNIPPET IS COMMENTED OUT
        "/$controller/$action?/$id?(.$format)?"{
            constraints {
                // apply constraints here
            }
        }*/

        "/"(view:"/index")
        "500"(view:'/error')
        "404"(view:'/notFound')
    }
}

And the following get.gsp:

<!doctype html>
<html>
<head>
    <meta name="layout" content="main"/>
    <title>Welcome to Grails</title>
</head>
<body>
  <g:form mapping="testMapping">
    <input type="submit" />
  </g:form>
</body>
</html>

When the submit button is pressed, a 404 is issued in the server. I expected that post() action forwards to get() action and the same view is showed again.

Task List

  • Steps to reproduce provided
  • Example that reproduces the problem uploaded to Github
  • Full description of the issue provided (see below)

Steps to Reproduce

  1. Clone https://github.com/maurociancio/grails-3-forward-issue
  2. $ grails run-app
  3. Hit http://localhost:8080/test-mapping in the browser and click submit button.
  4. 404 is shown.

Expected Behaviour

I expected the same view to be showed.

Actual Behaviour

404 is shown.

Environment Information

  • Operating System: Ubuntu 16.04
  • Grails Version: Tested with 3.3.8 and 3.3.6
  • JDK Version: java version "1.8.0_101"
    Java(TM) SE Runtime Environment (build 1.8.0_101-b13)
    Java HotSpot(TM) 64-Bit Server VM (build 25.101-b13, mixed mode)

Example Application

@ilopmar ilopmar self-assigned this Oct 8, 2018
@osscontributor
Copy link
Member

Fordward renders 404 error when should not

I don't think this has anything to do with a forward. The issue that your GSP is rendering a form that looks like this, which is invalid:

<form action="/test-mapping" method="post" >
    <input type="submit" />
</form>

I haven't looked into why that is happening, but I don't think it is related to your forward.

@maurociancio
Copy link
Author

Hi @jeffbrown thanks for your reply. Why is that form invalid?
When I perform GET to http://localhost:8080/test-mapping I get the page (no error). When I submit the form (or I POST to http://localhost:8080/test-mapping) I get an 404.
Thanks!

@osscontributor
Copy link
Member

@maurociancio Apologies. You are 100% correct. I mis-read it.

Sorry for the noise.

@maurociancio
Copy link
Author

Hi @jeffbrown, no problem! Is this a bug or am I misusing the API?
Thanks again!

@ilopmar
Copy link
Contributor

ilopmar commented Oct 9, 2018

Hi @maurociancio I think this is a valid bug. The issue is that during the forward method, the url to forward to is calculated as /test/get (using controllerName + actionName) instead of using the entry in the UrlMappings /test-mapping.
If you uncomment the block

"/$controller/$action?/$id?(.$format)?"{
    constraints {
    }
}

It works.

I'm working on a fix as we speak.

@maurociancio
Copy link
Author

Hey @ilopmar, thanks for your reply!
Please let me know when you have the fix so I can test it!
Thanks again!

@ilopmar
Copy link
Contributor

ilopmar commented Oct 10, 2018

Hi @maurociancio, can you please why you are doing a forward instead of a redirect? After debugging this a little bit more I'm not really sure if this is a bug or not, so I would like to know what you try to achieve with this.

@maurociancio
Copy link
Author

Hi @ilopmar! Of course, the example I provided was pretty small.
I use that pattern a lot when trying to create new entities in my app. For example:

class Controller {
  def create() {
    [command: request.command ?: new MyCommand()]
  }

  def doCreate(MyCommand cmd) {
    if (!cmd.hasErrors()) {
      new Entity().save()
      // redirect to somewhere else
    } else {
      forward action: 'create', model: [command: cmd]
    }
  }
}

So, when the validation fails in doCreate() I forward to the create() action so I can show the form again and provide feedback to the user about the validation errors. If I use redirect I would lose the command object between requests. Of course, I could render the create view again instead of forwarding, but I'd have to copy-paste the code in create() or extract a new method to have the duplicate code.

Is my example clear enough?
Thanks, let me know if you need a more complex demo app.

@ilopmar
Copy link
Contributor

ilopmar commented Oct 10, 2018

Why do you "lose the command object between request". You can do something like:

redirect action: 'create', model: [command: cmd]

Which is the same as you have in the else branch but using redirect instead of forward. Your create action already handles receiving a command.
What am I missing?

@maurociancio
Copy link
Author

I see your point, but: are the meanings of redirect and forward the same? I mean, when you forward() there no serialization of your command object. In the same request the command object is passed from one action to another. The user does not see the params. I could pass "private" parameters between the two actions, and the user would not notice.

When you redirect() the command object has to be serialized in the query string, right? So I could potencially be leaking some private parameters between the two actions.

So, they are similar but the semantics are different, right?
Please let me know if I got something wrong.
Thanks!

@ilopmar
Copy link
Contributor

ilopmar commented Oct 10, 2018

Yes, you are right. When doing a redirect all the parameters are displayed on the browser and appended to the url query string.
I never used the pattern you mention before when I was developing Grails applications with GSPs. What I did was something like:

if (cmd.hasErrors) {
   render view: 'create', model:[cmd: cmd,...]
} else {
   redirect view: 'show', id: object.id
}

So, if there is an error in the validation, instead of forwarding I render a view and I can pass any parameter I want.

@maurociancio
Copy link
Author

Hi @ilopmar!
Great. I could do that (I dont want to, because I'd have to review all the code), but given that I was migrating a grails 2 app to grails 3, and my example worked before, I reported this.
Is there any chance this could be fixed? Or maybe if I submit a pull request, will it get merged?
Thanks!

@ilopmar
Copy link
Contributor

ilopmar commented Oct 10, 2018

I'm still debugging this and tried a fix before but I broke other thing :-P
I haven't finished yet so I can't say it won't be fixed. If you submit a PR with tests we will review it and merge it. PR are always welcome :)

@ilopmar
Copy link
Contributor

ilopmar commented Oct 10, 2018

BTW, in the mean time, if you don't want to change your code you can enable the default Url Mappings or create specific entries for the affected actions.

@maurociancio
Copy link
Author

Hi @ilopmar! Okay! Thanks!
In what branch are you working on?

@ilopmar
Copy link
Contributor

ilopmar commented Oct 10, 2018

I haven't pushed anything yet so feel free to fork the repo and create your own branch.

@maurociancio
Copy link
Author

Haha, I could use some inspiration!

@ilopmar
Copy link
Contributor

ilopmar commented Oct 10, 2018

Hi @maurociancio. I've been discussing this issue with @jameskleeh and this is finally not a bug per-se.
What I'm going to do is fix it to allow execute a forward using the same http method. So if you hit an action using GET you will be allowed to forward the execution to another action reachable using GET.
In your use case, when forwarding from POST to GET, you will have an error different than a 404 but you will need to change your code to do a redirect or render a view depending on the case.

@maurociancio
Copy link
Author

Hi @ilopmar! Thanks for your reply!
I don't fully understand why this is not a bug. Would you please explain me?
Thanks!

@jameskleeh
Copy link
Contributor

@maurociancio Because you can't change the method in a forward. The forward to an action will look up the url mapping for the action and forward to that URL. Since the URL is the same for both of your actions, the original action will be invoked because the action you've attempted to forward to only responds to GET and the original request is a POST. That will lead to a stackoverflow.

@jameskleeh
Copy link
Contributor

POST /test-mapping -> forward determines URL for action get() -> dispatch to /test-mapping (still POST!) -> stackoverflow

@maurociancio
Copy link
Author

Haha, that makes sense now! Thanks for your time!

ilopmar added a commit that referenced this issue Oct 19, 2018
graemerocher added a commit that referenced this issue Oct 26, 2018
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Projects
None yet
Development

No branches or pull requests

4 participants