javajax-rsresteasy

How to obtain the name of a bad QueryParam in the custom BadRequest message?


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 IllegalArgumentExceptions 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

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.


Solution

  • 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.

    1. Pass in the parameter name as you suggest into MyParamConverter.
    2. Instead of using an IllegalArgumentException, create a new exception like ParameterViolationException which contains the parameter name.