Home > Software design >  In Quarkus/RESTEasy, how do I show helpful error messages for malformed query parameters?
In Quarkus/RESTEasy, how do I show helpful error messages for malformed query parameters?

Time:01-26

I have a REST endpoint that looks like this:

@Path("/api/v1/users")
@Produces(MediaType.APPLICATION_JSON)
@Consumes(MediaType.APPLICATION_JSON)
public class UsersResource {
  @GET
  public Uni<ListUsersResponse> list(@BeanParam @Valid ListUsersRequest req) {
    ...
  }
}
  
...

public class ListUsersRequest {
  @NotNull
  @NotEmpty
  @QueryParam("status_in")
  List<UserStatus> statusIn;
}

...

public enum UserStatus {
  All,
  Creating,
  Deleting,
  ...
}

When I make an API call like this:

http://localhost:8080/api/v1/users?status_in=invalid_status

Quarkus/RESTEasy will return a 404, and nothing will even show up in the server logs, because it is a 404, because it is a "client error".

If I enable debug logging, the error looks like this:

java.lang.IllegalArgumentException: No enum constant com.myproject.UserStatus.something

I dislike this behaviour, because:

  1. A 404 implies "not found", which makes users think that the path they used doesn't exist at all, which is confusing.
  2. It's difficult for both users and developers to figure out what went wrong, as there are no error messages in the response or in the server logs without DEBUG enabled.

Apparently, this is on purpose, because it conforms to some spec:

3.2 Fields and Bean Properties

if the field or property is annotated with @MatrixParam, @QueryParam or @PathParam then an implementation MUST generate an instance of NotFoundException (404 status) that wraps the thrown exception and no entity

So my question is, how can I modify this to actually give helpful error messages and status code?

CodePudding user response:

I have created a variety of exception handlers for just this reason. For example, I want to check if an incoming JSON body is malformed. For that I did:

import com.fasterxml.jackson.core.JsonParseException;
import com.fasterxml.jackson.databind.JsonMappingException;
import org.jboss.resteasy.spi.ReaderException;

import javax.ws.rs.core.MediaType;
import javax.ws.rs.core.Response;
import javax.ws.rs.ext.ExceptionMapper;
import javax.ws.rs.ext.Provider;

/**
 * Maps a ReaderException to a response.  A ReaderException is most often thrown
 * when we get a bad JSON packet.
 *
 */
@Provider
public class ReaderExceptionMapper implements ExceptionMapper<ReaderException> {

    @Override
    public Response toResponse(ReaderException exception) {

        if( exception.getCause() instanceof JsonParseException ||
                exception.getCause() instanceof JsonMappingException ) {
            return Response.status(Response.Status.BAD_REQUEST)
                    .entity("error parsing json body - "   exception.getMessage())
                    .type(MediaType.TEXT_PLAIN_TYPE)
                    .build();
        }

        return Response.status(Response.Status.INTERNAL_SERVER_ERROR)
                .entity(exception.getMessage())
                .type(MediaType.TEXT_PLAIN_TYPE)
                .build();
    }
}

In your case you should be able to create an ExceptionMapper for IllegalArgumentException and do what you'd like. I am running this in both Wildfly and Quarkus.

CodePudding user response:

You could use a ParamConverter for generic enum types.

import java.lang.annotation.Annotation;
import java.lang.reflect.Type;
import javax.ws.rs.WebApplicationException;
import javax.ws.rs.core.Response;
import javax.ws.rs.ext.ParamConverter;
import javax.ws.rs.ext.ParamConverterProvider;
import javax.ws.rs.ext.Provider;

@Provider
public class EnumParamConverterProvider implements ParamConverterProvider {
    @Override
    public <T> ParamConverter<T> getConverter(final Class<T> rawType, final Type genericType, final Annotation[] annotations) {
        if (!rawType.isEnum()) {
            return null;
        }
        final Enum<?>[] constants = (Enum<?>[]) rawType.getEnumConstants();
        return new ParamConverter<T>() {
            @Override
            @SuppressWarnings("unchecked")
            public T fromString(final String value) {
                if (value == null || value.isEmpty()) {
                    return null;
                }
                for (Enum<?> e : constants) {
                    if (e.name().equalsIgnoreCase(value)) {
                        return (T) e;
                    }
                }
                // No match, check toString()
                for (Enum<?> e : constants) {
                    if (e.toString().equalsIgnoreCase(value)) {
                        return (T) e;
                    }
                }
                throw new WebApplicationException(Response.serverError().entity(String.format("Failed to find constant '%s'", value)).build());
            }

            @Override
            public String toString(final T value) {
                return value != null ? value.toString() : null;
            }
        };
    }
}

CodePudding user response:

There is a similar question for which I gave an answer. Basically I suggested use of ResponseEntity class as a return value or creating Exception handler marked as @ControllerAdvice. Please look at the answer here: How to add message to response with exception

  •  Tags:  
  • Related