Consider this JAX-RS / RestEasy endpoint:
@GET
@Path("/my-endpoint")
@Produces(MediaType.TEXT_PLAIN)
public Response myEndpoint(@QueryParam("mySuperParam") @NotNull final MyParamType myParam) {
return ok(myParam.toString());
}
As you can see this is not a POST
query. There is no Json payload. We get the param from the query, as a QueryParam
. You can also see that I want to automatically convert the param to a custom type : MyParamType
// Simple wrapper around String. This is simplified for StackOverflow.
public record MyParamType(@Nullable String value) {
public MyParamType {
if(value == null)
return;
// Some custom validation
if(!value.contains("-")) {
throw new IllegalArgumentException("Is missing -");
}
}
@Override
public String toString() {
return value;
}
}
The conversion is done rather easily with a ParamConverter
and its ParamConverterProvider
:
import javax.ws.rs.ext.ParamConverter;
import javax.ws.rs.ext.ParamConverterProvider;
import javax.ws.rs.ext.Provider;
@Provider
public class MyParamConverterProvider implements ParamConverterProvider {
@Override
public <T> ParamConverter<T> getConverter(
final Class<T> rawType,
final Type genericType,
final Annotation[] annotations
) {
if (rawType.isAssignableFrom(MyParamType.class)) {
try {
return (ParamConverter<T>) new MyParamConverter();
} catch (final IllegalArgumentException e) {
// Here I DO have access to the QueryParam's name
final @NotNull String paramName = /* extract "mySuperParam" from the annotations just above*/
// POSSIBLE THROW LOCATION #1
throw new IllegalArgumentException("bad param:" + paramName);
}
}
return null;
}
import javax.ws.rs.ext.ParamConverter;
public class MyParamConverter implements ParamConverter<MyParamType> {
@Override
public MyParamType fromString(final String value) {
if (value == null) {
return null;
}
try {
return new MyParamType(value);
} catch (final IllegalArgumentException e) {
// Here I do NOT have access to the param's name
// POSSIBLE THROW LOCATION #2
throw new IllegalArgumentException(e.getMessage()); // Rethrowing. Keep reading to know why.
}
}
@Override
public @Nullable String toString(final @Nullable MyParamType value) {
if (value == null) {
return null;
}
return value.toString();
}
}
Now I want to catch the IllegalArgumentException
s and I want to convert them to BadRequest
.
I start by writing an ExceptionMapper
:
import javax.ws.rs.BadRequestException;
import javax.ws.rs.core.Response;
import javax.ws.rs.ext.ExceptionMapper;
import javax.ws.rs.ext.Provider;
@Provider
public class MyExceptionMapper implements ExceptionMapper<BadRequestException> {
@Override
public Response toResponse(final BadRequestException exception) {
return Response.status(Response.Status.BAD_REQUEST)
.entity("I wish I had the param name to show you!")
.build();
}
}
As per documentation, RestEasy will only let me apply ExceptionMapper<>
on an exception that is a WebApplicationException
(For example, ExceptionMapper<IllegalArgumentException>
gets ignored).
Nevermind then, all I have to do is to replace any of the two throw new IllegalArgumentException(...)
from above with throw new BadRequestException(...)
.
Problem : none of the two locations is entirely satisfactory
catch
block! Any exception thrown in POSSIBLE LOCATION #2 gets caught by RestEasy and converted to some HTTP Response.What is the proper way of returning a custom HTTP400 message that contains the name of the faulty QueryParam after my custom validation threw an exception?
Note: I could pass the annotations to the constructor of MyParamConverter
but my instinct tells me that there's a more standard solution that I'm missing.
There really isn't a way to get the parameter name in Jakarta REST or RESTEasy. The ParamConverter
is catching the exception and throwing a new one. You've got two options.
MyParamConverter
.IllegalArgumentException
, create a new exception like ParameterViolationException
which contains the parameter name.