Conversation
|
@all-contributors add @Ashfaqbs for code, bug |
|
I've put up a pull request to add @Ashfaqbs! 🎉 |
edeandrea
left a comment
There was a problem hiding this comment.
Thanks @Ashfaqbs for finding this! The unexpected-Content-Type branch is easy to miss because it only runs when the server misbehaves, and leaking the connection there is exactly the kind of bug that shows up later as pool exhaustion instead of a clear error. Thanks also for including a regression test that really guards the fix: I removed the closeQuietly(...) call locally and the test failed as it should. And thanks for explaining the Spotless reformat in the description. Our ratchetFrom("origin/main") config requires it once a file is touched, so there's nothing to change there.
I've left two small, non-blocking suggestions inline. Both are GitHub suggestion blocks, so you can add them all in one commit from the "Files changed" tab:
- Use try-with-resources in the
defaultbranch instead of thecloseQuietlyhelper. This matches the JSON branch just above it, keeps a failedclose()as a suppressed exception instead of dropping it, and doesn't throw aNullPointerExceptionif anHttpOperationsimplementation returns anullbody. - A companion test for the failing-
close()case.
I applied both locally: the tests pass and spotlessCheck is green.
| default -> { | ||
| closeQuietly(response.getBody()); | ||
| throw new DoclingServeClientException(null, "Invalid Content-Type in Task API response"); | ||
| } |
There was a problem hiding this comment.
Suggestion (non-blocking): try-with-resources would do the same job as closeQuietly and handle two edge cases better:
- Failed
close(): the Content-Type error is still what gets thrown, but theIOExceptionfromclose()is attached to it as a suppressed exception instead of being dropped, so it still shows up in logs and stack traces. nullbody: try-with-resources skips closing anullresource, so a customHttpOperationsthat returns no body gets the Content-Type error. WithcloseQuietlyit getsNullPointerException: Cannot invoke "java.io.InputStream.close()", which hides the real problem.
It also matches the CONTENT_TYPE_JSON branch above.
| default -> { | |
| closeQuietly(response.getBody()); | |
| throw new DoclingServeClientException(null, "Invalid Content-Type in Task API response"); | |
| } | |
| default -> { | |
| try (var ignored = response.getBody()) { | |
| throw new DoclingServeClientException(null, "Invalid Content-Type in Task API response"); | |
| } | |
| catch (IOException e) { | |
| // never reached in practice: a close() failure is added as suppressed to the exception above | |
| throw new DoclingServeClientException(e); | |
| } | |
| } |
The catch is only there because InputStream.close() declares IOException. The one exception that leaves the try is the unchecked DoclingServeClientException, so the comment says it isn't a real code path.
| private static void closeQuietly(InputStream inputStream) { | ||
| try { | ||
| inputStream.close(); | ||
| } | ||
| catch (IOException ignored) { | ||
| // the Content-Type error is the failure worth reporting | ||
| } | ||
| } | ||
|
|
There was a problem hiding this comment.
If you take the try-with-resources suggestion above, this helper is no longer needed:
| private static void closeQuietly(InputStream inputStream) { | |
| try { | |
| inputStream.close(); | |
| } | |
| catch (IOException ignored) { | |
| // the Content-Type error is the failure worth reporting | |
| } | |
| } |
| package ai.docling.serve.client.operations; | ||
|
|
||
| import java.io.IOException; | ||
| import java.io.InputStream; |
There was a problem hiding this comment.
...and this import is unused once closeQuietly goes:
| import java.io.InputStream; |
| import static org.assertj.core.api.Assertions.assertThatThrownBy; | ||
|
|
||
| import java.io.ByteArrayInputStream; | ||
| import java.io.InputStream; |
There was a problem hiding this comment.
Needed by the test suggested below:
| import java.io.InputStream; | |
| import java.io.IOException; | |
| import java.io.InputStream; |
| .isInstanceOf(DoclingServeClientException.class) | ||
| .hasMessageContaining("Invalid Content-Type"); | ||
| assertThat(closed).isTrue(); | ||
| } |
There was a problem hiding this comment.
Optional: a companion test for the failing-close() case. It fails on the current code (getSuppressed() is empty) and passes with the try-with-resources suggestion:
| } | |
| } | |
| @Test | |
| void convertTaskResultKeepsCloseFailureAsSuppressedOnUnexpectedContentType() { | |
| var body = new ByteArrayInputStream(new byte[0]) { | |
| @Override | |
| public void close() throws IOException { | |
| throw new IOException("close failed"); | |
| } | |
| }; | |
| var taskOperations = new TaskOperations(new StubHttpOperations(body, "text/html")); | |
| var request = TaskResultRequest.builder().taskId("task-1").build(); | |
| assertThatThrownBy(() -> taskOperations.convertTaskResult(request)) | |
| .isInstanceOf(DoclingServeClientException.class) | |
| .hasMessageContaining("Invalid Content-Type") | |
| .satisfies(t -> assertThat(t.getSuppressed()) | |
| .singleElement() | |
| .isInstanceOf(IOException.class) | |
| .hasFieldOrPropertyWithValue("message", "close failed")); | |
| } |
:java_duke: JaCoCo coverage report
|
|
||||||||||||||
|
HTML test reports are available as workflow artifacts (zipped HTML). • Download: Artifacts for this run |
convertTaskResult threw on an unrecognised Content-Type without closing the response body, leaking the underlying HTTP connection. Close it before throwing and cover the case with a unit test. Signed-off-by: Ashfaqbs <105435085+Ashfaqbs@users.noreply.github.com>
c95c319 to
9d29e1f
Compare
What
TaskOperations.convertTaskResultopens the result as a stream and switches on the Content-Type. The JSON branch closes the stream (try-with-resources) and the ZIP branch hands it to the caller, but thedefaultbranch threwInvalid Content-Type in Task API responsewithout closing it, so the underlying HTTP connection stayed open.Change
defaultbranch (a failed close is ignored, since the Content-Type error is the one worth reporting).TaskOperationsTests, which uses a stubHttpOperationsreturning atext/htmlresponse and asserts the exception is thrown and the body is closed. It fails without the fix.Spotless applied; the formatter also re-wrapped a few lines in the touched file.