Kotlin & Spring MVC - HTTP Status 400 - Null

Viewed 739

I have a Kotlin @RestController, and I would expect it to return 400 Bad Request in a situation where a null is passed for a @RequestParam argument.

Example:

@RestController
class Api() {

  @PostMapping("endpoint")
  fun endpoint(@DateTimeFormat(iso = DATE) @RequestParam date: LocalDate) {
    //do something
  }
}

If I were to make a request to POST /endpoint?date I get a 500 Internal Server Error with the following (shortened) body:

{
  "timestamp": "2020-09-14T20:39:38.102+0000",
  "status": 500,
  "error": "Internal Server Error",
  "message": "Parameter specified as non-null is null: method Api.endpoint, parameter date",
  "trace": "java.lang.IllegalArgumentException: Parameter specified as non-null is null: method Api.endpoint, parameter date\r\n\tat Api.endpoint(Api.kt)\r\n\t 
    ...
    ...
    atjava.base/java.lang.Thread.run(Thread.java:834)\r\n",
 "path": "/campaigns/contributions/unfunded/retries"
}

Is there any way to fix this using either some additional library, a configuration, or some custom code that does not have other side effects that keeps everything the same except that the status code will be 400 Bad Request

3 Answers

The following works for Kotlin 1.4.*. This is not necessarily the best answer because there is no guarantee that Kotlin will not change the excption type of message in future releases. For eaxample, when I originally asked the question I was using 1.3.*, and the exception was IllegalArgumentException. In 1.4.* it has been changed to NullPointerException.

@ControllerAdvice
class KotlinNonNullParamHandling {

    @ExceptionHandler
    protected fun handleKotlinNonNullViolation(
        exception: NullPointerException,
        response: HttpServletResponse
    ) {
        val nullParameter = exception.message?.startsWith("Parameter specified as non-null is null: ") == true
        val restController = exception
            .stackTrace
            .getOrNull(0)
            ?.let { Class.forName(it.className) }
            ?.getAnnotation(RestController::class.java) != null
        val status =
            if (nullParameter && restController) HttpStatus.BAD_REQUEST
            else HttpStatus.INTERNAL_SERVER_ERROR
        response.sendError(status.value(), exception.message)
    }
}

Before assuming that it is 400, we are checking that the exception

  • is a NullPointerException
  • has a message starting with "Parameter specified as non-null is null: "
  • it was thrown from a class the is RestController (otherwise it might be some other problem, such as a null being sent to a method by reflection having nothing to do with the web layer.)

Spring will throw the IllegalArgumentException by default when a validation fails here. Every exception inside your Spring-Boot application will be treated by default as InternalServerError. You can modify your response code by adding a function like this:

@RestController
class Api() {

  @ExceptionHandler(IllegalArgumentException.class)
  @ResponseStatus(HttpStatus.BAD_REQUEST)
  public void onIllegalArgumentException(IllegalArgumentException e) {
  }

  @PostMapping("endpoint")
  fun endpoint(@DateTimeFormat(iso = DATE) @RequestParam date: LocalDate) {
    //do something
  }
}

Afterwards you'll get the 400 status code on invalid request param.

I've tested your code, and there's nothing obviously wrong with it - works as expected.

curl -v -d "date=2020-10-01" http://localhost:8080/endpoint
*   Trying ::1:8080...
* TCP_NODELAY set
* Connected to localhost (::1) port 8080 (#0)
> POST /endpoint HTTP/1.1
> Host: localhost:8080
> User-Agent: curl/7.68.0
> Accept: */*
> Content-Length: 15
> Content-Type: application/x-www-form-urlencoded
> 
* upload completely sent off: 15 out of 15 bytes
* Mark bundle as not supporting multiuse
< HTTP/1.1 200 
< Content-Length: 0
< Date: Tue, 13 Oct 2020 07:04:21 GMT
< 
* Connection #0 to host localhost left intact

So I would:

  1. Verify that the client is properly formatting the request - e.g. try explicitly setting the Content-Type header to application/x-www-form-urlencoded, check if there's no typo in the param name etc.
  2. That you have Jackson properly configured to work with Kotlin (com.fasterxml.jackson.module:jackson-module-kotlin - dependency)
Related