简体   繁体   中英

Spring Boot PostMapping: How to enforce JSON decoding if no Content-Type present

I'm implementing a simple rest controller with Spring Boot (2.3.2) to receive data from an external source via POST. That source doesn't send a Content-Type with it, but provides a body containing JSON.

My controller is really simple

@RestController
@RequiredArgsConstructor
public class WaitTimeImportController {
    private final StorageService storageService;

    @PostMapping(value = "/endpoint")
    public void setWaitTimes(@RequestBody DataObject toStore) {
        storageService.store(toStore);
    }
}
@Data
@NoArgsConstructor
class DataObject {
  private String data;
}
@Service
class StorageService {
    void store(DataObject do) {
        System.out.println("received");
    }
}

Now, if I send it a json String, it will not trigger that request mapping.

@SpringBootTest(webEnvironment = SpringBootTest.WebEnvironment.RANDOM_PORT)
@ActiveProfiles("test")
class WaittimesApplicationTests {

    @Test
    void happyPath() throws IOException {
        WebClient client = client();

        String content = "{ \"data\": \"test\"} ";
        AtomicBoolean ab = new AtomicBoolean();
        await().atMost(Duration.FIVE_SECONDS).ignoreExceptions()
            .until(() -> {
                ClientResponse response = client.post()
                        // .header("Content-Type", "application/json")
                        .bodyValue(content)
                        .exchange()
                        .timeout(java.time.Duration.ofMillis(200))
                        .block();
        ab.set(response.statusCode() == HttpStatus.ACCEPTED);
                return ab.get();
            });
    }
}

This test fails unless I uncomment the "header" line above; then it is fine.

Since I have no influence over the source submitting the POST request, I cannot add application/json as the request header.

I've been searching a lot about content negotiation in Spring, but nothing I've tried worked.

  • adding a consumes="application/json" to the @PostMapping annotation
  • adding a consumes=MediaType.ALL_VALUE to the @PostMapping annotation
  • adding a MappingJackson2HttpMessageConverter bean didn't work
  • adding a ContentNegotiationStrategy bean didn't compile "cannot access javax.servlet.ServletException" - is it a worthwhile route to pursue this?

What can I do to enforce the mapping to accept a non-"Content-Type" request and decode it as JSON?

Uggg. So Lebecca's comment sent on a wild run through how to adjust header field with Spring (I'm using webflux, so the linked solution was not applicable as such), and I couldn't find a proper tutorial on it and the documentation is a bit distributed amongst several pages you come across when searching.

There are a few roadblocks, so here's a condensed version.

Clean way: Adjusting the header to inject the Content-Type`

So what you can do is create a WebFilter that gets evaluated before the request actually reaches the mapping. Just define a @Component that implements it.

To add to the ServerWebExchange object that gets passed, you wrap it in a ServerWebExchangeDecorator .

@Component
class MyWebFilter implements WebFilter {

    @Override
    public Mono<Void> filter(ServerWebExchange exchange, WebFilterChain chain) {
        return chain.filter(new MyServerExchangeDecorator(exchange));
    }
}

That decorator can manipulate both the request and the response; just override the appropriate method. Of course, the request needs to be decorated as well - can't go around working on the original object (it actually throws an exception if you try to set its properties because it's read only).

class MyServerExchangeDecorator extends ServerWebExchangeDecorator {

    protected MyServerExchangeDecorator(ServerWebExchange delegate) {
        super(delegate);
    }
    @Override
    public ServerHttpRequest getRequest() {
        return new ContentRequestDecorator(super.getRequest());
    }
}

Now you just implement the ContentRequestDecorator and you're good to go.

class ContentRequestDecorator extends ServerHttpRequestDecorator {

    public ContentRequestDecorator(ServerHttpRequest delegate) {
        super(delegate);
    }
    @Override
    public HttpHeaders getHeaders() {
        HttpHeaders httpHeaders = HttpHeaders.writableHttpHeaders(super.getHeaders());
        httpHeaders.setContentType(MediaType.APPLICATION_JSON);

        return httpHeaders;
    }
}

Trying to just call setContentType on the object being passed in results in an OperationNotSupportedException because, again, it is read only; hence the HttpHeaders.writableHttpHeaders() call.

Of course, this will adjust all headers for all incoming requests, so if you plan on doing this and don't want the nuclear option, inspect the request beforehand.

The simple way

So the way above triggers Springs mapping of the incoming request to a consumes="application/json" endpoint.

However, if you didn't read my previous description as containing a slightly sarcastic undertone, I should have been less subtle. It's a very involved solution for a very simple problem.

The way I actually ended up doing it was

    @Autowired
    private ObjectMapper mapper;

    @PostMapping(value = "/endpoint")
    public void setMyData(HttpEntity<String> json) {
        try {
            MyData data = mapper.readValue(json.getBody(), MyData.class);
            storageService.setData(data);
        } catch (JsonProcessingException e) {
            return ResponseEntity.unprocessableEntity().build();
        }
    }

The technical post webpages of this site follow the CC BY-SA 4.0 protocol. If you need to reprint, please indicate the site URL or the original address.Any question please contact:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM