yashmayya commented on code in PR #18515:
URL: https://github.com/apache/pinot/pull/18515#discussion_r3261125032


##########
pinot-broker/src/main/java/org/apache/pinot/broker/api/resources/PinotClientRequest.java:
##########
@@ -176,6 +178,7 @@ public void processSqlQueryGet(@ApiParam(value = "Query", 
required = true) @Quer
   @ApiOperation(value = "Querying pinot")
   @ApiResponses(value = {
       @ApiResponse(code = 200, message = "Query response"),
+      @ApiResponse(code = 400, message = "Bad Request"),

Review Comment:
   **Backward-incompat surfacing — add release notes / doc updates.**
   
   This PR is correctly labeled `backward-incompat`, but the operator-facing 
surface hasn't been updated. Clients/dashboards keyed on 
`UNCAUGHT_POST_EXCEPTIONS` or HTTP-500 error-rate alarms will see a 
step-change, and any client that previously retried on 5xx for malformed JSON / 
missing fields will now get a non-retriable 400.
   
   Please add a release-notes block / CHANGELOG entry calling out:
   1. The 500→400 status code change for the five affected endpoints.
   2. The new `BAD_REQUEST_EXCEPTIONS` meter.
   3. The expected drop in `UNCAUGHT_POST_EXCEPTIONS` counts.



##########
pinot-broker/src/main/java/org/apache/pinot/broker/api/resources/PinotClientRequest.java:
##########
@@ -219,29 +230,40 @@ public void processSqlQueryPost(String query, @Suspended 
AsyncResponse asyncResp
           + "Supports both single-stage and multi-stage queries.")
   @ApiResponses(value = {
       @ApiResponse(code = 200, message = "Query fingerprint"),
+      @ApiResponse(code = 400, message = "Bad Request"),
       @ApiResponse(code = 500, message = "Internal Server Error")
   })
   @ManualAuthorization
   public Response getQueryFingerprint(String query,
       @Context org.glassfish.grizzly.http.server.Request requestContext,
       @Context HttpHeaders httpHeaders) {
     try {
-      JsonNode requestJson = JsonUtils.stringToJsonNode(query);
+      JsonNode requestJson;
+      try {
+        requestJson = JsonUtils.stringToJsonNode(query);
+      } catch (JsonProcessingException e) {
+        throw new BadRequestException("Invalid JSON: " + e.getMessage(), e);
+      }
       if (!requestJson.has(Request.SQL)) {
-        throw new IllegalStateException("Payload is missing the query string 
field 'sql'");
+        throw new BadRequestException("Payload is missing the query string 
field 'sql'");
       }
 
       QueryFingerprint fingerprint = generateQueryFingerprint((ObjectNode) 
requestJson);
 
       return Response.ok(fingerprint).build();
+    } catch (BadRequestException bre) {
+      _brokerMetrics.addMeteredGlobalValue(BrokerMeter.BAD_REQUEST_EXCEPTIONS, 
1L);
+      throw bre;

Review Comment:
   **Inconsistent error response body between 400 and 500 paths.**
   
   The 500 path a few lines below now correctly emits `{"error": "..."}` JSON 
(good — fixes the JSON-injection bug). But this new 400 path re-throws the raw 
`BadRequestException`, which goes through Jersey's default exception mapper and 
produces a different body shape (often plain text or empty body depending on 
configured providers).
   
   Callers that previously parsed `{"error": "..."}` for *all* failures now get 
a structurally different body for 400s — a contract break for any tooling that 
parses error responses.
   
   Suggest wrapping so the body shape stays consistent across status codes:
   ```java
   ObjectNode errorJson = JsonUtils.newObjectNode();
   errorJson.put("error", bre.getMessage());
   throw new WebApplicationException(bre,
       
Response.status(Response.Status.BAD_REQUEST).entity(errorJson.toString()).build());
   ```



##########
pinot-broker/src/main/java/org/apache/pinot/broker/api/resources/PinotClientRequest.java:
##########
@@ -472,10 +520,15 @@ public void 
processSqlQueryWithBothEnginesAndCompareResults(String query, @Suspe
       CompletableFuture.allOf(v1Response, v2Response).join();
 
       asyncResponse.resume(getPinotQueryComparisonResponse(v1Query, 
v1Response.get(), v2Response.get()));
+    } catch (BadRequestException bre) {

Review Comment:
   **Async-path `BadRequestException`s still get reclassified as 500 here.**
   
   The `v1Response` / `v2Response` `supplyAsync` lambdas above wrap thrown 
exceptions as `new RuntimeException(e)`. When 
`CompletableFuture.allOf(...).join()` rethrows, the result is 
`CompletionException` → `RuntimeException` → original cause. This `catch 
(BadRequestException bre)` clause will NOT match that wrapped chain, so a 
400-worthy error raised deep inside `executeSqlQuery` still falls into the 
generic `catch (Exception e)` below and emits 500 + `UNCAUGHT_POST_EXCEPTIONS`.
   
   Three options:
   1. Unwrap `CompletionException` / `RuntimeException` causes here before 
classifying.
   2. Refactor the `supplyAsync` lambdas not to double-wrap.
   3. If `executeSqlQuery` is contractually guaranteed not to throw 
`BadRequestException` (it does return error responses via 
`BrokerResponseNative` rather than throw), document that with a comment so 
future readers don't trip on it. Please confirm which case applies.



##########
pinot-broker/src/main/java/org/apache/pinot/broker/api/resources/PinotClientRequest.java:
##########
@@ -493,8 +546,10 @@ public void 
processSqlQueryWithBothEnginesAndCompareResults(String query, @Suspe
       + "given id on the requested broker. Query may continue to run for a 
short while after calling cancel as "
       + "it's done in a non-blocking manner. The cancel method can be called 
multiple times.")
   @ApiResponses(value = {
-      @ApiResponse(code = 200, message = "Success"), @ApiResponse(code = 500, 
message = "Internal server error"),
-      @ApiResponse(code = 404, message = "Query not found on the requested 
broker")
+      @ApiResponse(code = 200, message = "Success"),
+      @ApiResponse(code = 400, message = "Bad Request"),

Review Comment:
   **Swagger 400 message is too generic across endpoints.**
   
   The same `"Bad Request"` text is used here and on all four SQL endpoints, 
which makes the generated API docs less useful. Suggest endpoint-specific 
descriptions, e.g.:
   - Here: `"Invalid query id format"`
   - SQL endpoints: `"Missing/invalid SQL field or malformed JSON"`
   
   Also worth documenting that for `cancelQuery`, the 400 only applies to 
internal-id parsing — `cancelQueryByClientId` (the `isClient=true` branch) 
doesn't parse a `Long` and won't surface 400 for malformed client ids.



##########
pinot-broker/src/test/java/org/apache/pinot/broker/api/resources/PinotClientRequestTest.java:
##########
@@ -171,16 +173,65 @@ public void testPinotQueryComparisonApiDifferentQuery() 
throws Exception {
   }
 
   @Test
-  public void testPinotQueryComparisonApiMissingSql() throws Exception {
+  public void testPinotQueryComparisonApiMissingV2Sql() throws Exception {

Review Comment:
   **Status-code contract isn't validated end-to-end.**
   
   All new tests assert on `WebApplicationException.getResponse().getStatus()` 
— the *exception object's* status — which proves the right exception was raised 
but not that Jersey actually returns HTTP 400 on the wire. Since this PR's 
whole purpose is the HTTP wire-level contract, the assertions are necessary but 
not sufficient.
   
   Please either:
   1. Add at least one Jersey-level integration test (or `JerseyTest`-based 
test) that POSTs a malformed body and asserts the actual HTTP response status; 
or
   2. Add a comment at the catch sites in `PinotClientRequest` documenting that 
the wire-level 400 relies on JAX-RS's default `BadRequestException → 400` 
mapping, so future refactors don't accidentally break it.



##########
pinot-broker/src/test/java/org/apache/pinot/broker/api/resources/PinotClientRequestTest.java:
##########
@@ -274,10 +325,103 @@ public void testGetQueryFingerprintWithInvalidSql() 
throws Exception {
     when(request.getRequestURL()).thenReturn(new StringBuilder());
 
     String requestJson = "{\"sql\": \"INVALID SQL QUERY\"}";
-    Response response = _pinotClientRequest.getQueryFingerprint(requestJson, 
request, null);
+    try {
+      _pinotClientRequest.getQueryFingerprint(requestJson, request, null);
+      Assert.fail("Expected BadRequestException");
+    } catch (WebApplicationException wae) {
+      assertEquals(wae.getResponse().getStatus(), 
Response.Status.BAD_REQUEST.getStatusCode(),
+          "Invalid SQL query should return BAD_REQUEST status");
+    }
+    
verify(_brokerMetrics).addMeteredGlobalValue(BrokerMeter.BAD_REQUEST_EXCEPTIONS,
 1L);
+    verify(_brokerMetrics, 
never()).addMeteredGlobalValue(BrokerMeter.UNCAUGHT_POST_EXCEPTIONS, 1L);
+  }
+
+  @Test
+  public void testProcessSqlQueryPostMissingSql() {
+    AsyncResponse asyncResponse = mock(AsyncResponse.class);
+    Request request = mock(Request.class);
+    when(request.getRequestURL()).thenReturn(new StringBuilder());
+
+    _pinotClientRequest.processSqlQueryPost("{}", asyncResponse, false, 0, 
request, null);
+
+    ArgumentCaptor<WebApplicationException> captor = 
ArgumentCaptor.forClass(WebApplicationException.class);
+    verify(asyncResponse).resume(captor.capture());
+    assertEquals(captor.getValue().getResponse().getStatus(), 
Response.Status.BAD_REQUEST.getStatusCode());
+    
verify(_brokerMetrics).addMeteredGlobalValue(BrokerMeter.BAD_REQUEST_EXCEPTIONS,
 1L);
+    verify(_brokerMetrics, 
never()).addMeteredGlobalValue(BrokerMeter.UNCAUGHT_POST_EXCEPTIONS, 1L);
+  }
+
+  @Test
+  public void testProcessSqlQueryPostInvalidJson() {
+    AsyncResponse asyncResponse = mock(AsyncResponse.class);
+    Request request = mock(Request.class);
+    when(request.getRequestURL()).thenReturn(new StringBuilder());
+
+    _pinotClientRequest.processSqlQueryPost("{bad json", asyncResponse, false, 
0, request, null);
+
+    ArgumentCaptor<WebApplicationException> captor = 
ArgumentCaptor.forClass(WebApplicationException.class);
+    verify(asyncResponse).resume(captor.capture());
+    assertEquals(captor.getValue().getResponse().getStatus(), 
Response.Status.BAD_REQUEST.getStatusCode());
+    
verify(_brokerMetrics).addMeteredGlobalValue(BrokerMeter.BAD_REQUEST_EXCEPTIONS,
 1L);
+    verify(_brokerMetrics, 
never()).addMeteredGlobalValue(BrokerMeter.UNCAUGHT_POST_EXCEPTIONS, 1L);
+  }
+
+  @Test
+  public void testGetQueryFingerprintMissingSql() {
+    Request request = mock(Request.class);
+    when(request.getRequestURL()).thenReturn(new StringBuilder());
+
+    try {
+      _pinotClientRequest.getQueryFingerprint("{}", request, null);
+      Assert.fail("Expected BadRequestException");
+    } catch (WebApplicationException wae) {
+      assertEquals(wae.getResponse().getStatus(), 
Response.Status.BAD_REQUEST.getStatusCode());
+    }
+    
verify(_brokerMetrics).addMeteredGlobalValue(BrokerMeter.BAD_REQUEST_EXCEPTIONS,
 1L);
+    verify(_brokerMetrics, 
never()).addMeteredGlobalValue(BrokerMeter.UNCAUGHT_POST_EXCEPTIONS, 1L);
+  }
+
+  @Test
+  public void testGetQueryFingerprintInvalidJson() {
+    Request request = mock(Request.class);
+    when(request.getRequestURL()).thenReturn(new StringBuilder());
+
+    try {
+      _pinotClientRequest.getQueryFingerprint("{bad json", request, null);
+      Assert.fail("Expected BadRequestException");
+    } catch (WebApplicationException wae) {
+      assertEquals(wae.getResponse().getStatus(), 
Response.Status.BAD_REQUEST.getStatusCode());
+    }
+    
verify(_brokerMetrics).addMeteredGlobalValue(BrokerMeter.BAD_REQUEST_EXCEPTIONS,
 1L);
+    verify(_brokerMetrics, 
never()).addMeteredGlobalValue(BrokerMeter.UNCAUGHT_POST_EXCEPTIONS, 1L);
+  }
+
+  @Test
+  public void testCancelQueryWithInvalidId() {
+    try {
+      _pinotClientRequest.cancelQuery("not-a-number", false, 3000, false);

Review Comment:
   **`cancelQuery` `isClient=true` branch is uncovered.**
   
   This test only exercises `isClient=false`. The cancel-flow rewrite touches 
both branches — `isClient=true` passes the raw `String` id to 
`_requestHandler.cancelQueryByClientId` and never parses `Long`, so the new 
`BadRequestException` path is dead code for that branch.
   
   Please add a sibling test for `cancelQuery("anything", true, 3000, false)` 
to lock in the expected behavior (likely 404 from `cancelQueryByClientId` 
returning `false` against the unstubbed mock).



-- 
This is an automated message from the Apache Git Service.
To respond to the message, please log on to GitHub and use the
URL above to go to the specific comment.

To unsubscribe, e-mail: [email protected]

For queries about this service, please contact Infrastructure at:
[email protected]


---------------------------------------------------------------------
To unsubscribe, e-mail: [email protected]
For additional commands, e-mail: [email protected]

Reply via email to