Die Response Falle – HTTP Response Status in Spring MVC Controllern

Heutzutage ist doch alles REST, auch Sachen die gar nicht Restful sind.. aber das ist eine andere Diskussion.

Rest-Architekturen basieren auf dem HTTP Protokoll. Ein besonders wichtiger Aspekt des Protokolls sind HTTP Status Codes. Über Status-Codes teilt der Server einem Client mit, ob eine Operation erfolgreich war (Status Code 200), ob ein Fehler aufgetreten ist  (u.a. Status Code 500) oder ob die Parameter nicht den erwarteten entsprechen (Code 412).

Es ist also sehr wichtig, dem Aufrufer den richtigen Status-Code zurückzuliefern, damit dieser ggf. darauf reagieren kann.

Das Beispiel aus dem letzten Artikel führen wir einfach ein wenig weiter.

Wir haben einen sehr einfachen MVC Controller definiert.

@Controller
@EnableAutoConfiguration
public class SimpleSpringController {

    public SimpleSpringController() {
        System.out.println("Log");
    }

    @RequestMapping(
        method = RequestMethod.GET,
        value = "/hello/{helloValue}",
        produces = MediaType.APPLICATION_XML_VALUE
    )
    @ResponseBody
    public Hello sayHello(@PathVariable(value = "helloValue") String hello){
          //assume some persistence is going on here
          return new Hello(hello);
    }

    public static void main( String[] args )
    {
        SpringApplication.run(SimpleSpringController.class, args);
    }
}

Die Methode sayHello liefert ein Hello-Objekt zurück und lässt sich über das URL Pattern “/hello/{helloValue}” ansprechen.

Per Default liefert ein Controller den Status-Code 200 zurück, was als “Alles OK” interpretiert werden kann. Nehmen wir an, dass wir beim Aufruf nicht nur ein neues Objekt zurückgeben, sondern dieses auch persistieren möchten.

Man könnte die Methode dann so modellieren.

@RequestMapping(
        method = RequestMethod.PUT,
        value = "/hello/{helloValue}",
        produces = MediaType.APPLICATION_XML_VALUE
    )
    @ResponseBody
    public Hello sayHello(@PathVariable(value = "helloValue") String hello){
          //assume some persistence is going on here
          return new Hello(hello);
    }

Wir ändern die Methode auf PUT und möchten zusätzlich den Status Code 201 zurückliefern. Überfliegt man die Dokumentation, so könnte man annehmen, dass folgender Ansatz vielversprechend wäre.

@RequestMapping(
        method = RequestMethod.GET,
        value = "/hello/{helloValue}",
        produces = MediaType.APPLICATION_XML_VALUE
    )
    @ResponseBody
    @ResponseStatus(value = HttpStatus.CREATED)
    public Hello sayHello(@PathVariable(value = "helloValue") String hello){
        //do some fancy persistence stuff
        return new Hello(hello);
    }

Methoden lassen sich mit @ResponseStatus annotieren und liefern den richtigen Status-Code zurück.

Das ist aber eigentlich falsch, denn @ResponseStatus ist für ExceptionHandling gedacht. Mit dieser Annotation lassen sich sowohl Exceptions annotieren als auch ExceptionHandler. Warum aber nicht Controller? Der obige Fall würde funktionieren und die Methode liefert den Status-Code 201 zurück.

Das lässt sich auch sehr einfach mit einem kleinen Testcase beweisen.

@Test
public void testControllerResponseStatus() throws Exception
{
	MockMvc mockMvc = MockMvcBuilders.standaloneSetup(new SimpleSpring                         Controller()).build();
	mockMvc.perform(get("/hello/test").accept(
                 MediaType.APPLICATION_XML)).
            andExpect(status().isCreated());
}

Wo liegt also das Problem?

Die Annotation hat einen zweiten Parameter reason.

    @ResponseStatus(value = HttpStatus.CREATED, reason = "some reason")

Das große Problem ist, wird reason gesetzt ruft Spring automatisch die HttpServletResponse.sendError Methode auf. In diesem Fall liefert die Response einen leeren Body, egal was der eigentliche Return-Value ist.

Extrem gefährlich und tritt nur zur Laufzeit auf.

Wir sehen es noch nicht mal in unserem obigen Testcase, denn der Call ist nach wie vor erfolgreich und liefert den Status Code 201 Created zurück. Erweitern wir den Testfall um ein Unmarshalling via JAXB.

 @Test
	public void testControllerResponseStatus() throws Exception
	{
        MockMvc mockMvc = MockMvcBuilders.standaloneSetup(new SimpleSpringController()).build();
        mockMvc.perform(get("/hello/test").accept(MediaType.APPLICATION_XML)).andExpect(
            new ResultMatcher()
            {
                @Override
                public void match(MvcResult result) throws Exception
                {
                    JAXBContext context = JAXBContext.newInstance(Hello.class);
                    Hello hello = (Hello)context.createUnmarshaller().unmarshal(
                        new ByteArrayInputStream(result.getResponse()
                            .getContentAsByteArray()));
                    assertEquals("test", hello.getHello());
                }
            });
	}

Jetzt endlich schlägt der Test fehl mit folgender Fehlermeldung.

javax.xml.bind.UnmarshalException
 - with linked exception:
[org.xml.sax.SAXParseException; lineNumber: 1; columnNumber: 1; Vorzeitiges Dateiende.]

Der ResponseBody ist leer, was er nicht sein sollte und deswegen beschwert sich JAXB zurecht.

Da ich selbst über diesen Fehler gestolpert bin dient dieser Blogeintrag als Reminder (hauptsächlich für mich).

Links

Spring MVC Testing Tutorial von Petri Kainulainen

Pre-Spring 3.2.x MVC Testing

Effective Trainings & Consulting - Martin Dilger



Hat Ihnen dieser Blog-Eintrag gefallen? Ich stelle in diesem Blog Informationen über Tools, Frameworks und Werkzeuge zur Verfügung, die mich produktiver machen. Vielleicht kann ich auch Ihnen helfen, produktiver zu werden.


Ich unterstütze Sie als freier Mitarbeiter bei der Entwicklung von Software-Projekten, Agiler Arbeit sowie Schulungen / Fortbildungen.


Jeden Tag ein bisschen produktiver - ab heute