4

I am trying to write a controller with Spring MVC 3.2 that consumes/produces both JSON and HTML. I have two handler methods that produce different content types:

@Controller
public class FooController {
    @RequestMapping(value="/foo", produces="text/html")
    public String fooHTML() {
        // ...
    }

    @RequestMapping(value="/foo", produces="application/json")
    public String fooJSON() {
        // ...
    }
}

This works splendidly if the Accept header from the client contains either text/html or application/json,

...but then there was Internet Explorer. As noted here, IE's Accept header varies, but it never contains text/html and always has */* at the end. When Spring receives a request from IE, it sees no content types directly equal to the ones produced by my controller, but, latching on to the */* wildcard, it (correctly) decides that both mappings would apply.

Faced with multiple matching handler mappings, Spring (in the RequestMappingHandlerMapping bean) sorts the mappings by what essentially amounts to lexicographic order, picks the first one, and moves on. The problem, from my perspective, is that this process prioritizes application/json over text/html. I would much rather return text/html unless the client specifically requests application/json — that way, I can serve HTML to dumb clients like IE, and JSON to content-type-savvy clients like users of my API.

Does anyone know of a way to do this that doesn't require extending RequestMappingHandlerMapping to sort the handlers differently? Do you have any simple workarounds?

NOTE: I've tried setting a default content type in the ContentNegotiationManager as described on the Spring blog. It doesn't solve my problem, because that setting only goes into effect when no Accept header is specified.

Tim Yates
  • 5,151
  • 2
  • 29
  • 29

2 Answers2

1

One solution is to lower the fooJSON() priority by it's value parameter.

In practice, the patterns /foo and /foo{1} are equivalent. The second one, though, is considered "more generic" and is used lastly:

@Controller
public class FooController {
    @RequestMapping(value="/foo", produces="text/html")
    public String fooHTML() {
        // ...
    }

    @RequestMapping(value="/foo{1}", produces="application/json")
    //                         ^^^------- changed here
    public String fooJSON() {
        // ...
    }
}

This way:

  • text/html goes to fooHTML()
  • application/json goes to fooJSON()
  • */* goes to fooHTML()
  • anything else yields a 406 - Not Acceptable error
acdcjunior
  • 132,397
  • 37
  • 331
  • 304
  • What do you mean by "anything else"? Is there something which will not map to `*/*`??? – Pavel Horal Jun 06 '13 at 20:10
  • Ignore my comment... just realized what you meant by it. – Pavel Horal Jun 06 '13 at 20:11
  • Very creative. I certainly wouldn't have thought of it. Unfortunately, it constrains me in certain ways that aren't acceptable. I can't get both /foo/ and /foo/some-foo-id/ to respond to both content types, because if I declare a `/foo/{1}` -> JSON mapping and a `/foo/{someID}/` -> HTML mapping, they overlap, and /foo/some-foo-id/ with `*/*` picks the JSON one. – Tim Yates Jun 06 '13 at 21:17
  • I guess should add that that situation is a problem when I have the controller *class* mapped to `/foo` and two *methods* mapped to `{1}` -> JSON and `/{someID}/` -> HTML. It's actually not possible to make a JSON handler that responds to /foo/ otherwise. The bottom line is that it just eliminates a lot of options. But it still could be a valuable technique in some places. – Tim Yates Jun 06 '13 at 21:26
  • 1
    Hm, I'm not sure I follow. Can you give an exact example? (If you wanted the pattern `/foo/{someID}`, the JSON one would be `/foo{1}/{someID}`.) – acdcjunior Jun 06 '13 at 22:09
  • D'oh. I was only trying combinations with the dummy path variable at the end of the string. That seems to work perfectly. – Tim Yates Jun 07 '13 at 13:18
1

I've found one way to get around this issue is to add an ALL mimetype to your request mapping that serves the HTML version of the response.

@RequestMapping(value="/foo", produces={"text/html", "*/*"})
public String fooHTML() {
    // ...
}

This does mean that the HTML response is served for any previously unmapped mime types. In my case, that behaviour was desired, but if you need to return a 406 (Not Acceptable) response code, then this won't work for you.