spring-projects / spring-hateoas

Spring HATEOAS - Library to support implementing representations for hyper-text driven REST web services.
https://spring.io/projects/spring-hateoas
Apache License 2.0
1.03k stars 475 forks source link

Unable to marshal Resources - HAL should be used #692

Open mariuszs opened 6 years ago

mariuszs commented 6 years ago

Invalid content type is used, for Resources endpoint in HAL enabled application.

This requires special code @GetMapping(produces = {MediaType.APPLICATION_JSON_VALUE, "application/hal+json"}) for endpoint method returning Resources.

Logged error:

.w.s.m.s.DefaultHandlerExceptionResolver : Failed to write HTTP message: org.springframework.http.converter.HttpMessageNotWritableException: Could not marshal [Resources { content: [FooApplication.FooResource(foo=foo)], links: [] }]: null; nested exception is javax.xml.bind.MarshalException
 - with linked exception:
[com.sun.istack.internal.SAXException2: unable to marshal type "com.foo.FooApplication$FooResource" as an element because it is missing an @XmlRootElement annotation]

Sample application:

@SpringBootApplication
public class FooApplication {

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

    @RestController("/foos")
    public class FooController {

        @GetMapping
        public Resources<FooResource> foo() {
            List<FooResource> foos = Stream.of(new FooResource("foo")).collect(Collectors.toList());
            return new Resources(foos);
        }
    }

    @Data
    public class FooResource extends ResourceSupport {
        private final String foo;
    }

}

Dependencies:

    <dependencies>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-hateoas</artifactId>
        </dependency>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-web</artifactId>
        </dependency>

        <dependency>
            <groupId>org.projectlombok</groupId>
            <artifactId>lombok</artifactId>
            <optional>true</optional>
        </dependency>
    </dependencies>

This is similar to bug: https://github.com/spring-guides/tut-bookmarks/issues/8

gregturn commented 6 years ago

With Spring MVC, you can also use standard content negotiation (which removes the need for a produces clause). You simply request GET /foos.json, and it will generate a JSON response, which in this case should default to HAL.

gregturn commented 6 years ago

Alternatively, sending an Accept: application/hal+json header would trigger Spring MVC to force a HAL response.

P.S. Spring HATEOAS has a MediaTypes class containing constants for the supported hypermedia types, saving you from managing that raw string value of application/hal+json.

mariuszs commented 6 years ago

@gregturn This is true, and this works for most cases like @GetMapping public FooResource foo(), but there is a bug in spring-hateos and this do not work for Resources.

In attached code is example without produces clause, and this fails with attached exception. So standard content negotiation is not working here.

mariuszs commented 6 years ago

Hmm, ok. This is because my browser sends Accept:text/html,application/xhtml+xml,application/xml;q=0.9,image/webp,image/apng,*/*;q=0.8. This is strange, because this works from browser for others endpoints (like single resource).

mariuszs commented 6 years ago
    @RestController
    @RequestMapping(value = "/foos")
    public class FooController {

        @GetMapping
        public Resources<FooResource> foos() {
            List<FooResource> foos = Stream.of(new FooResource("foo")).collect(Collectors.toList());
            return new Resources(foos);
        }

        @GetMapping("/{fooId}")
        public FooResource foo(@PathVariable String fooId) {
            return new FooResource(fooId);
        }
    }

Command curl -H 'accept: application/xml,*/*' http://localhost:8080/foos/bar

Returns:

HTTP/1.1 200 
Content-Type: application/hal+json;charset=UTF-8

{"foo":"bar"}

but curl -H 'accept: application/xml,*/*' http://localhost:8080/foos

HTTP/1.1 500 
Content-Type: application/xhtml+xml

<html><body><h1>Whitelabel Error Page</h1><p>This application has no explicit mapping for /error, so you are seeing this as a fallback.</p><div id='created'>Tue Jan 30 21:18:40 CET 2018</div><div>There was an unexpected error (type=Internal Server Error, status=500).</div><div>Could not marshal [Resources { content: [FooApplication.FooResource(foo=foo)], links: [] }]: null; nested exception is javax.xml.bind.MarshalException
 - with linked exception:
[com.sun.istack.internal.SAXException2: unable to marshal type &quot;com.foo.FooApplication$FooResource&quot; as an element because it is missing an @XmlRootElement annotation]</div></body></html>⏎      

This is somehow inconsistent.

mariuszs commented 6 years ago

@gregturn Can we do something with content negotiation for Resources?

gregturn commented 6 years ago

Why are you asking for XML? The exception is about something wrong in the rendering of an XML response.

I'll confess that the types of Spring HATEOAS include support for XML renderings but the support is only halfway there. Because JSON is what is most popular today. Those wanting XML probably want SOAP anyway.

So what happens in your scenario when you request application/Hal+JSON on the resources aggregate?

mariuszs commented 6 years ago

It works just fine for curl -H 'accept: application/hal+json' http://localhost:8080/foos and for curl http://localhost:8080/foos.

The reason why I'm asking for XML is because I was tested this endpoint from browser (like many others people), and browser sends text/html,application/xhtml+xml,application/xml;q=0.9,image/webp,image/apng,*/*;q=0.8. After checking this header it fails for this combination application/xml,*/*.

For example there is no error 500 for application/xml.

# curl -I -H 'accept: application/xml'  http://localhost:8080/foos

HTTP/1.1 406 
# curl  -H 'accept: */*'  http://localhost:8080/foos

HTTP/1.1 200 
Content-Type: application/hal+json;charset=UTF-8

{"_embedded":{"foos":[{"foo":"foo"}]}}
# curl -I -H 'accept: application/xml,*/*'  http://localhost:8080/foos

HTTP/1.1 500 
Content-Type: application/json;charset=UTF-8
gregturn commented 6 years ago

Okay, so the nub of everything is you want XML? That wasn't evident when you're opening concern was having to apply the produces clause inside @GetMapping(produces = {MediaType.APPLICATION_JSON_VALUE, "application/hal+json"}) to get a HAL JSON document.

Basically, there is no support for HAL XML at this point in time (application/hal+xml), frankly because there hasn't been a huge demand, as stated before. If you look at MediaTypes you can see what is supported, and these are all JSON formats.

mariuszs commented 6 years ago

Oh, no. I want JSON. And generally all works smooth and I receive JSON. But when you open this HAL endpoint with resources list, then you get error 500, because for this specific endpoint hateos wants to use XML instead.

This is exactly the same problem like reported here: https://github.com/spring-guides/tut-bookmarks/issues/8

People are opening this endpoints in browser, and when endpoint return Resources then this blow up, but works just fine from curl. For endpoint returning single Resource there is no problem at all.

All is about inconsistency, different browsing result for Resources and Resource. I hope this is more clear now.

mariuszs commented 6 years ago

In my opinion, when we have HAL activated, and browser sends accept: application/xml;*/* then server should return application/hal+json. Why it is trying to send XML, if there is no XML support for HAL? application/hal+json is what browser accepts and should be used instead.

mariuszs commented 6 years ago

??

gregturn commented 6 years ago

Basically, when you ask to XML (which is what is listed in the first accept type), and Spring HATEOAS thanks to its XML annotations says that it can, it commits to going down that path.

I crafted a branch (https://github.com/spring-projects/spring-hateoas/tree/bug/handle-browser) with all of its XML annotations removed, and this issue goes away. I'm trying to consult with @olivergierke to get his opinion on the matter.

gregturn commented 5 years ago

We’ve removed all JAXB annotations. If you could give the latest version a spin and see if the issue still persists I’d appreciate it.