7

My Java spring REST API controller looks like this:

public void signup(@RequestBody RequestBody requestBody) throws IOException, ServletException {

I get this exception:

Failed to read HTTP message: org.springframework.http.converter.HttpMessageNotReadableException: Could not read document: Stream closed; nested exception is java.io.IOException: Stream closed

This happens because I want to cast the request body to RequestBody class (which opens the request input stream and finishes it), and also forward/redirect it to another endpoint.

The actual controller is:

    @RequestMapping(value = "/signup", method = RequestMethod.POST)
    public void signup(@RequestBody CustomUserDetails user, HttpServletRequest request, HttpServletResponse response) {

        String userName = user.getUsername();
        logger.debug("User signup attempt with username: " + userName);

        try{
            if(customUserDetailsService.exists(userName))
            {
                logger.debug("Duplicate username " + userName);
userName + " already exists");
                String newUrl = "login";
                RequestDispatcher view = request.getRequestDispatcher(newUrl);
                view.forward(request, response);
            } else {
                customUserDetailsService.save(user);
                authenticateUserAndSetSession(user, response);
            }
        } catch(Exception ex) {

        }
    }

How should I handle this ?

Arian
  • 7,397
  • 21
  • 89
  • 177
  • 1
    Why would you want the request instead of the serialized object? You cannot cast the `HttpServletRequest` as that obviously isn't a `RequestBody` instance. You would need to parse the request (i.e. read the `InputStream` and convert that into your object). But adding them both should simply work (if it doesn't you must be trying something you shouldn't be doing in the first place). – M. Deinum May 09 '17 at 06:55
  • No it doesn't work. I tried to add @RequestBody, HttpServletReques, HttpServletResponse to the method signature and later it failed reading the input stream, when I removed the first arg (RequestBody) it worked. I want to read the casted RequestBody object in the controller and forward it to another controller. – Arian May 09 '17 at 07:07
  • You cannot read the input stream (again) as that can only be consumed once. Why are you reading the input stream again. (And trust me it works as have been using it on multiple projects). – M. Deinum May 09 '17 at 07:16
  • Why don't you just use only the first syntax you have provided? I mean, why would you bother using the `HttpServletRequest`? – dounyy May 09 '17 at 09:56
  • I want to forward the request to another endpoint after processing the requestBody in this controller. – Arian May 09 '17 at 16:17
  • Just a suggestion: you could just return a OK or USEREXIST kind of response and call automatically the login controller from client code based on the response – Zeromus May 12 '17 at 09:35
  • have you tried returning `"redirect:/someurl"` – Derrops May 15 '17 at 02:16

6 Answers6

5

You can forward to login page in a ExceptionHandler,like this:

@RequestMapping(value = "/signup", method = RequestMethod.POST)
public void signup(@RequestBody CustomUserDetails user, HttpServletResponse response) {

    String userName = user.getUsername();
    logger.debug("User signup attempt with username: " + userName);

    //try{
    if (customUserDetailsService.exists(userName)) {
        logger.debug("Duplicate username " + userName);
        throw new SignupException(userName + " already exists");
    } else {
        customUserDetailsService.save(user);
        authenticateUserAndSetSession(user, response);
    }
    /*} catch(Exception ex) {

    }*/
}

define a ExceptionHandler in the same Controller:

@ExceptionHandler(SignupException.class)
public String duplicateName() {
    return "login";
}

and the SignupException could be like this:

public class SignupException extends RuntimeException {
    public SignupException(String message) {
        super(message);
    }

    public SignupException() {
    }
}
rayen
  • 89
  • 7
4

The request body object is a stream which can be read only once. So forwarding it is not very trivial. One way around this is to create a filter which reads the input steam and replace the input stream to something which can be read multiple times. Example can be found at another answer:

How can I read request body multiple times in Spring 'HandlerMethodArgumentResolver'?

As for your method, there is also another problem:

public void signup(@RequestBody RequestBody requestBody)

As far as I know, RequestBody is an annotation and you can't map it like that. But to get the raw data, you can map it as String.

public void signup(@RequestBody String requestBody)

And then you can just manually make an REST call to the api you want to forward it to using the String request body. Just make sure you set the content-type as the original one, which I assume in this case would be JSON.

Community
  • 1
  • 1
Rowanto
  • 2,819
  • 3
  • 18
  • 26
4

Root of your problem is using @RequestBody RequestBody requestBody together with HttpServletRequest request.

Opening input stream twice on the same request is not allowed. In your case a system should open a input stream to extract request body and then propagate in forward to reuse.

To handle it you should avoid multiple usage of the same request stream. Possible solutions are:

  1. Wrap request
  2. Copy request body
  3. Use spring native forward
Sergei Rybalkin
  • 3,337
  • 1
  • 14
  • 27
2

I think you are trying to forward to a url with the RequestBody, please have a check Spring 3.2 forward request with new object for the answers.

Create the object and add it to the request as an attribute in the first controller,

request.setAttribute("user",user),
return "forward:/login";
Community
  • 1
  • 1
Jane
  • 657
  • 8
  • 14
1

Try putting in request mapping consumes= {" application/json"}, produces={"application/json"}

vinay verma
  • 656
  • 6
  • 8
  • Please complete your answer and add details. I don't understand it. – Arian May 12 '17 at 03:55
  • @RequestMapping(value = "/signup", method = RequestMethod.POST, = {" application/json"}, produces={"application/json"} ) – vinay verma May 12 '17 at 04:01
  • How is it supposed to fix the issue ? I don't get it. – Arian May 12 '17 at 04:36
  • you suppose to tell your controller that what data format are you expecting and what you will be sending back as response.... i have seen that error before though i'm very beginner to this but it should solve your problem... and if it does please let me know :) – vinay verma May 13 '17 at 04:55
1
  1. RequestBody is an annotation to process your request object as expected class deserialization. They help you avoid boilerplate code by extracting the logic of messageconversion and making it an aspect.
  2. You can not get RequestBody directly as an object in any controller. It was not designed to use this way.
  3. Though it is not possible to get RequestBody in a RestController so you can't judge whether it is good or bad practice.
  4. If you have to use new controller/endpoint to process.Then i think the better approach is to get the CustomUserDetails as @RequestBody in the controller then process it. Then call nested method or service to further process instead of thinking forwarding to another controller. Then return response from the controller.
Zico
  • 2,349
  • 2
  • 22
  • 25