diff --git a/src/main/java/com/retailsvc/http/internal/RequestPreparationFilter.java b/src/main/java/com/retailsvc/http/internal/RequestPreparationFilter.java index 68014e0..3365d25 100644 --- a/src/main/java/com/retailsvc/http/internal/RequestPreparationFilter.java +++ b/src/main/java/com/retailsvc/http/internal/RequestPreparationFilter.java @@ -243,7 +243,16 @@ private ParsedBody validateAndParseBody(HttpExchange exchange, Operation op, byt new ValidationError( BODY_POINTER, "content-type", "unsupported content type: " + mediaType, null)); } - Object parsed = mapper.readFrom(body, header); + Object parsed; + try { + parsed = mapper.readFrom(body, header); + } catch (RuntimeException e) { + // Body could not be parsed (e.g. malformed JSON). Untrusted input -> 400, not 500. + LOG.debug("Failed to parse request body", e); + throw new ValidationException( + new ValidationError( + BODY_POINTER, "malformed", "request body is not valid " + mediaType, null)); + } if (mediaType.equals("application/x-www-form-urlencoded") && parsed instanceof Map map) { @SuppressWarnings("unchecked") Map typed = (Map) map; diff --git a/src/test/java/com/retailsvc/http/OpenApiServerIT.java b/src/test/java/com/retailsvc/http/OpenApiServerIT.java index 00b724b..680bd06 100644 --- a/src/test/java/com/retailsvc/http/OpenApiServerIT.java +++ b/src/test/java/com/retailsvc/http/OpenApiServerIT.java @@ -127,6 +127,28 @@ void postDataShouldReturnJsonBody() { } } + @Test + void postDataShouldReturnBadRequestOnMalformedJson() throws Exception { + try (var server = newServer(Map.of("post-data", new EchoHandler())); + var client = httpClient()) { + + // Double comma after a property is invalid JSON; must be 400, not 500. + var body = "{\"id\":\"some-id\",,\"age\":42}"; + var headers = Map.of("correlation-id", UUID.randomUUID().toString()); + var request = newRequest(server, path, "POST", ofString(body), headers); + + var response = client.send(request, BodyHandlers.ofString()); + var statusCode = response.statusCode(); + var contentType = response.headers().firstValue("Content-Type").orElse(""); + var responseBody = response.body(); + + assertThat(statusCode).isEqualTo(400); + assertThat(contentType).contains("application/problem+json"); + assertThat(responseBody).contains("keyword"); + assertThat(responseBody).contains("pointer"); + } + } + @Test void postDataShouldReturnBadRequestOnMissingRequiredProperties() { Map handlers = Map.of("post-data", new EchoHandler()); @@ -217,7 +239,7 @@ void listObjectsShouldReturnJsonBody() { } @Test - void listObjectsShouldReturnBadRequestOnPassingObjectInsteadOfArray() { + void listObjectsShouldReturnBadRequestOnPassingObjectInsteadOfArray() throws Exception { try (var server = newServer(Map.of("post-list-objects", new EchoHandler())); var client = httpClient()) { @@ -235,12 +257,6 @@ void listObjectsShouldReturnBadRequestOnPassingObjectInsteadOfArray() { assertThat(contentType).contains("application/problem+json"); assertThat(responseBody).contains("keyword"); assertThat(responseBody).contains("pointer"); - - } catch (IOException e) { - fail(e); - } catch (InterruptedException e) { - Thread.currentThread().interrupt(); - fail(e); } } } @@ -448,7 +464,7 @@ void postShapeUnknownKindReturns400() { } @Test - void postShapeMissingDiscriminatorReturns400() { + void postShapeMissingDiscriminatorReturns400() throws Exception { // omitting "kind" makes both branches fail "required". try (var server = newServer(Map.of("post-shape", new EchoHandler())); var client = httpClient()) { @@ -464,11 +480,6 @@ void postShapeMissingDiscriminatorReturns400() { // Both branches fail identically at /kind required -> de-duplicated to one entry. assertThat(response.body()).contains("\"errors\"").contains("#/kind"); assertThat(response.body().split("#/kind", -1)).hasSize(2); // exactly one occurrence - } catch (IOException e) { - fail(e); - } catch (InterruptedException e) { - Thread.currentThread().interrupt(); - fail(e); } } }