Rest servers should return 400 for bad syntax JSON 45/94245/2
authorJim Hahn <jrh3@att.com>
Fri, 23 Aug 2019 22:35:40 +0000 (18:35 -0400)
committerJim Hahn <jrh3@att.com>
Fri, 23 Aug 2019 22:46:14 +0000 (18:46 -0400)
Modified common RestServer to inject an exception handler
into the list of providers so that it returns 400 instead
of 500 for JSON parsing errors.

Change-Id: I7c77625e3531e26413d72cc386296a709946cda2
Issue-ID: POLICY-1725
Signed-off-by: Jim Hahn <jrh3@att.com>
policy-endpoints/src/main/java/org/onap/policy/common/endpoints/http/server/JsonExceptionMapper.java [new file with mode: 0644]
policy-endpoints/src/main/java/org/onap/policy/common/endpoints/http/server/RestServer.java
policy-endpoints/src/test/java/org/onap/policy/common/endpoints/http/server/test/JsonExceptionMapperTest.java [new file with mode: 0644]
policy-endpoints/src/test/java/org/onap/policy/common/endpoints/http/server/test/RestServerTest.java

diff --git a/policy-endpoints/src/main/java/org/onap/policy/common/endpoints/http/server/JsonExceptionMapper.java b/policy-endpoints/src/main/java/org/onap/policy/common/endpoints/http/server/JsonExceptionMapper.java
new file mode 100644 (file)
index 0000000..9ee72f0
--- /dev/null
@@ -0,0 +1,56 @@
+/*-
+ * ============LICENSE_START=======================================================
+ * Copyright (C) 2019 AT&T Intellectual Property. All rights reserved.
+ * ================================================================================
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ *
+ * SPDX-License-Identifier: Apache-2.0
+ * ============LICENSE_END=========================================================
+ */
+
+package org.onap.policy.common.endpoints.http.server;
+
+import com.google.gson.JsonSyntaxException;
+import javax.ws.rs.Produces;
+import javax.ws.rs.core.MediaType;
+import javax.ws.rs.core.Response;
+import javax.ws.rs.ext.ExceptionMapper;
+import javax.ws.rs.ext.Provider;
+import lombok.Getter;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+/**
+ * Catches JSON exceptions when decoding a REST request and converts them from an HTTP 500
+ * error code to an HTTP 400 error code.
+ */
+@Provider
+@Produces(MediaType.APPLICATION_JSON)
+public class JsonExceptionMapper implements ExceptionMapper<JsonSyntaxException> {
+    private static Logger logger = LoggerFactory.getLogger(JsonExceptionMapper.class);
+
+    @Override
+    public Response toResponse(JsonSyntaxException exception) {
+        logger.warn("invalid JSON request", exception);
+        return Response.status(Response.Status.BAD_REQUEST).entity(new SimpleResponse("Invalid JSON request")).build();
+    }
+
+    @Getter
+    private static class SimpleResponse {
+        private String errorDetails;
+
+        public SimpleResponse(String errorDetails) {
+            this.errorDetails = errorDetails;
+        }
+    }
+}
index 597375b..2368c0b 100644 (file)
@@ -99,7 +99,7 @@ public class RestServer extends ServiceManagerContainer {
         props.setProperty(svcpfx + PolicyEndPointProperties.PROPERTY_AAF_SUFFIX,
                         String.valueOf(restServerParameters.isAaf()));
         props.setProperty(svcpfx + PolicyEndPointProperties.PROPERTY_HTTP_SERIALIZATION_PROVIDER,
-                        GsonMessageBodyHandler.class.getName());
+                        String.join(",", GsonMessageBodyHandler.class.getName(), JsonExceptionMapper.class.getName()));
         return props;
     }
 
diff --git a/policy-endpoints/src/test/java/org/onap/policy/common/endpoints/http/server/test/JsonExceptionMapperTest.java b/policy-endpoints/src/test/java/org/onap/policy/common/endpoints/http/server/test/JsonExceptionMapperTest.java
new file mode 100644 (file)
index 0000000..3efbf85
--- /dev/null
@@ -0,0 +1,50 @@
+/*-
+ * ============LICENSE_START=======================================================
+ * ONAP
+ * ================================================================================
+ * Copyright (C) 2019 AT&T Intellectual Property. All rights reserved.
+ * ================================================================================
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ * ============LICENSE_END=========================================================
+ */
+
+package org.onap.policy.common.endpoints.http.server.test;
+
+import static org.junit.Assert.assertEquals;
+
+import com.google.gson.JsonSyntaxException;
+import javax.ws.rs.core.Response;
+import org.junit.Before;
+import org.junit.Test;
+import org.onap.policy.common.endpoints.http.server.JsonExceptionMapper;
+import org.onap.policy.common.utils.coder.CoderException;
+import org.onap.policy.common.utils.coder.StandardCoder;
+
+public class JsonExceptionMapperTest {
+    private JsonExceptionMapper mapper;
+
+    @Before
+    public void setUp() {
+        mapper = new JsonExceptionMapper();
+    }
+
+    @Test
+    public void testToResponse() throws CoderException {
+        JsonSyntaxException ex = new JsonSyntaxException("expected exception");
+        Response resp = mapper.toResponse(ex);
+
+        assertEquals(Response.Status.BAD_REQUEST.getStatusCode(), resp.getStatus());
+        assertEquals("{'errorDetails':'Invalid JSON request'}".replace('\'', '"'),
+                        new StandardCoder().encode(resp.getEntity()));
+    }
+}
index 3f67173..fd9b59f 100644 (file)
@@ -23,15 +23,29 @@ package org.onap.policy.common.endpoints.http.server.test;
 import static org.assertj.core.api.Assertions.assertThatIllegalArgumentException;
 import static org.junit.Assert.assertEquals;
 import static org.junit.Assert.assertNotNull;
+import static org.junit.Assert.assertTrue;
 import static org.mockito.Matchers.any;
 import static org.mockito.Mockito.mock;
 import static org.mockito.Mockito.never;
 import static org.mockito.Mockito.verify;
 import static org.mockito.Mockito.when;
 
+import java.io.IOException;
+import java.io.PrintWriter;
+import java.net.HttpURLConnection;
+import java.net.URL;
 import java.util.Arrays;
+import java.util.Base64;
 import java.util.Properties;
 import javax.servlet.http.HttpServletRequest;
+import javax.ws.rs.Consumes;
+import javax.ws.rs.POST;
+import javax.ws.rs.Path;
+import javax.ws.rs.Produces;
+import javax.ws.rs.core.MediaType;
+import javax.ws.rs.core.Response;
+import lombok.Getter;
+import org.junit.After;
 import org.junit.AfterClass;
 import org.junit.Before;
 import org.junit.BeforeClass;
@@ -39,12 +53,15 @@ import org.junit.Test;
 import org.mockito.ArgumentCaptor;
 import org.onap.policy.common.endpoints.http.server.HttpServletServer;
 import org.onap.policy.common.endpoints.http.server.HttpServletServerFactory;
+import org.onap.policy.common.endpoints.http.server.JsonExceptionMapper;
 import org.onap.policy.common.endpoints.http.server.RestServer;
 import org.onap.policy.common.endpoints.http.server.RestServer.Factory;
 import org.onap.policy.common.endpoints.http.server.aaf.AafAuthFilter;
 import org.onap.policy.common.endpoints.parameters.RestServerParameters;
 import org.onap.policy.common.endpoints.properties.PolicyEndPointProperties;
 import org.onap.policy.common.gson.GsonMessageBodyHandler;
+import org.onap.policy.common.utils.coder.StandardCoder;
+import org.onap.policy.common.utils.network.NetworkUtil;
 import org.powermock.reflect.Whitebox;
 
 public class RestServerTest {
@@ -58,6 +75,7 @@ public class RestServerTest {
     private static final String USER = "my-user";
     private static Factory saveFactory;
 
+    private RestServer realRest;
     private RestServer rest;
     private HttpServletServer server1;
     private HttpServletServer server2;
@@ -101,6 +119,18 @@ public class RestServerTest {
         when(params.isHttps()).thenReturn(true);
 
         Whitebox.setInternalState(RestServer.class, FACTORY_FIELD, factory);
+
+        realRest = null;
+    }
+
+    /**
+     * Stops the rest server.
+     */
+    @After
+    public void tearDown() {
+        if (realRest != null) {
+            realRest.stop();
+        }
     }
 
     @Test
@@ -181,10 +211,58 @@ public class RestServerTest {
         assertEquals(PASS, props.getProperty(svcpfx + PolicyEndPointProperties.PROPERTY_HTTP_AUTH_PASSWORD_SUFFIX));
         assertEquals("true", props.getProperty(svcpfx + PolicyEndPointProperties.PROPERTY_HTTP_HTTPS_SUFFIX));
         assertEquals("true", props.getProperty(svcpfx + PolicyEndPointProperties.PROPERTY_AAF_SUFFIX));
-        assertEquals(GsonMessageBodyHandler.class.getName(),
+        assertEquals(String.join(",", GsonMessageBodyHandler.class.getName(), JsonExceptionMapper.class.getName()),
                         props.getProperty(svcpfx + PolicyEndPointProperties.PROPERTY_HTTP_SERIALIZATION_PROVIDER));
     }
 
+    @Test
+    public void testInvalidJson() throws Exception {
+        when(params.getHost()).thenReturn("localhost");
+        when(params.getPort()).thenReturn(NetworkUtil.allocPort());
+        when(params.isHttps()).thenReturn(false);
+        when(params.isAaf()).thenReturn(false);
+
+        // use real factory
+        Whitebox.setInternalState(RestServer.class, FACTORY_FIELD, saveFactory);
+
+        realRest = new RestServer(params, null, RealProvider.class) {
+            @Override
+            protected Properties getServerProperties(RestServerParameters restServerParameters, String names) {
+                Properties props = super.getServerProperties(restServerParameters, names);
+
+                String svcpfx = PolicyEndPointProperties.PROPERTY_HTTP_SERVER_SERVICES + "."
+                                + restServerParameters.getName();
+                props.setProperty(svcpfx + PolicyEndPointProperties.PROPERTY_HTTP_SWAGGER_SUFFIX, "false");
+
+                return props;
+            }
+        };
+
+        realRest.start();
+        assertTrue(NetworkUtil.isTcpPortOpen(params.getHost(), params.getPort(), 100, 100));
+
+        assertEquals(200, roundTrip(new StandardCoder().encode(new MyRequest())));
+        assertEquals(400, roundTrip("{'bogus-json'"));
+    }
+
+    private int roundTrip(String request) throws IOException {
+        URL url = new URL("http://" + params.getHost() + ":" + params.getPort() + "/request");
+        HttpURLConnection conn = (HttpURLConnection) url.openConnection();
+        conn.setDoInput(true);
+        conn.setDoOutput(true);
+        conn.setRequestMethod("POST");
+        String auth = params.getUserName() + ":" + params.getPassword();
+        conn.setRequestProperty("Authorization", "Basic " + Base64.getEncoder().encodeToString(auth.getBytes()));
+        conn.setRequestProperty("Content-type", MediaType.APPLICATION_JSON);
+        conn.connect();
+
+        try (PrintWriter wtr = new PrintWriter(conn.getOutputStream())) {
+            wtr.write(request);
+        }
+
+        return conn.getResponseCode();
+    }
+
     @Test
     public void testToString() {
         rest = new RestServer(params, Filter.class, Provider1.class, Provider2.class);
@@ -220,4 +298,25 @@ public class RestServerTest {
             // do nothing
         }
     }
+
+    @Path("/")
+    @Produces(MediaType.APPLICATION_JSON)
+    @Consumes(MediaType.APPLICATION_JSON)
+    public static class RealProvider {
+        @POST
+        @Path("/request")
+        public Response decision(MyRequest body) {
+            return Response.status(Response.Status.OK).entity(new MyResponse()).build();
+        }
+    }
+
+    @Getter
+    public static class MyRequest {
+        private String data;
+    }
+
+    @Getter
+    public static class MyResponse {
+        private String text = "hello";
+    }
 }