Repository navigation
Conversation
The onRequestContent callback in Jetty's async pipeline may not fire for small PUT requests on macOS with proxy authentication or TLS/HTTP2, leaving progressedCount at 0 even though the upload succeeds. This is because Jetty can coalesce the request content with headers in its HTTP/2 send pipeline, bypassing the per-chunk callback notification. After a successful PUT, if no progress event was reported via the onRequestContent callback, fire transportStarted/transportProgressed retroactively using the task's data. This ensures the progress listener is always informed of completed uploads regardless of Jetty's internal callback timing. Fix apache#2203 Co-authored-by: Claude Opus 4 <noreply@anthropic.com>
gnodet-bot
left a comment
There was a problem hiding this comment.
Reviewed the retroactive progress-event path added to fix the macOS flakiness. The logic is correct and well-contained behind the progressed.get() guard.
One low-severity memory observation below.
This review was generated by an AI agent, Hermès on behalf of @gnodet.
gnodet-bot
left a comment
There was a problem hiding this comment.
Previous finding (memory concern with readAllBytes()) is addressed — the second commit switches to the chunked 32 KB buffer approach. One new concern below.
This review was generated by an AI agent, Hermès on behalf of @gnodet.
The retroactive transportStarted/transportProgressed block runs after the server returned 2xx, so the upload has already succeeded. If a listener throws TransferCancelledException here, the caller sees an exception from implPut() and may retry a PUT that already landed. Catch and discard TCE in both calls: for transportStarted(), return early (no point continuing notification); for transportProgressed(), break out of the loop (same reasoning).
gnodet-bot
left a comment
There was a problem hiding this comment.
Re-review after commit 3 (Guard retroactive progress path against TransferCancelledException)
Previous findings addressed:
- ✅ Memory concern (
readAllBytes()) — fixed with chunked 32 KB buffer (commit 2) - ✅ Uncaught
TransferCancelledExceptionin retroactive path — bothtransportStartedandtransportProgressednow guarded (commit 3)
Two new observations below. One inline (ordering) and one general (test coverage).
Missing dedicated test for the retroactive path: The existing tests testPut_ProxyAuthenticated and testPut_SSL assert listener.getProgressedCount() > 0, but on Linux (where CI runs) Jetty fires onRequestContent normally, so the retroactive block at lines 416-439 is never exercised by CI. The flakiness was macOS-specific — the fix cannot be proven correct by the current CI suite. A deterministic unit test that simulates the Jetty-skips-callback scenario (e.g. by mocking onRequestContent to never fire while still completing the PUT successfully) would close this gap.
This review was generated by an AI agent, Hermès on behalf of @gnodet.
gnodet-bot
left a comment
There was a problem hiding this comment.
Error-path regression: transportPropertiesAvailable unreachable on failed PUT requests
The move of transportPropertiesAvailable from before the status check to after it introduces a behavioral regression on error responses.
Before this PR:
response = listener.get(requestTimeout, TimeUnit.MILLISECONDS);
task.getListener().transportPropertiesAvailable(...); // ← called unconditionally
if (response.getStatus() >= MULTIPLE_CHOICES) {
JettyRFC9457Reporter.INSTANCE.generateException(...); // throws
}After this PR:
response = listener.get(requestTimeout, TimeUnit.MILLISECONDS);
if (response.getStatus() >= MULTIPLE_CHOICES) {
JettyRFC9457Reporter.INSTANCE.generateException(...); // throws — skips everything below
}
// ... retroactive progress block ...
task.getListener().transportPropertiesAvailable(...); // ← unreachable on 3xx/4xx/5xxFor error responses (status ≥ 300), generateException throws HttpTransporterException, which exits the try block before reaching transportPropertiesAvailable. Transport properties are therefore never populated for failed PUT transfers — a regression relative to both the old behavior and the Apache/Wagon HTTP transporter.
Fix: move transportPropertiesAvailable back to before the status check. The comment in the PR body says the ordering requirement is "after transportStarted", which is satisfied even in the pre-status-check position (since the retroactive progress block already fired). Example:
if (task.getDataLength() > 0 && !progressed.get()) {
// ... retroactive transportStarted/transportProgressed ...
}
// transportPropertiesAvailable goes here — BEFORE the status check
task.getListener().transportPropertiesAvailable(createTransportProperties(request, rawResponseHeaders));
if (response.getStatus() >= MULTIPLE_CHOICES) {
JettyRFC9457Reporter.INSTANCE.generateException(...);
}This review was generated by an AI agent, Hermès on behalf of @gnodet.
Move transportPropertiesAvailable before the status check so it is reachable on both success (2xx) and error (3xx/4xx/5xx) responses. Previously, generateException() threw HttpTransporterException for error responses, exiting the try block before reaching transportPropertiesAvailable — a regression from the original behavior. Also move the retroactive progress block before the status check so the listener ordering contract (transportPropertiesAvailable after transportStarted) is maintained regardless of response status.
gnodet-bot
left a comment
There was a problem hiding this comment.
Re-review after commit 5 (restore transportPropertiesAvailable on error responses)
Previous REQUEST_CHANGES finding addressed:
- ✅
transportPropertiesAvailableunreachable on error responses — fixed by moving it before thegenerateExceptionstatus check, with a clear explanatory comment
One new concern introduced by this commit: moving the retroactive progress block before the status check exposes a silent-failure path that didn't exist before.
This review was generated by an AI agent, Hermès on behalf of @gnodet.
…s block When the retroactive progress block runs before the status check (as of commit 5), catching TransferCancelledException with 'return' exits implPut() without calling generateException — silently swallowing error responses (3xx/4xx/5xx). Replace 'return' with a fall-through so the status check is always reached regardless of TCE.
gnodet-bot
left a comment
There was a problem hiding this comment.
Re-review after commit 6 (fall through status check when TCE thrown in retroactive progress block)
Previous REQUEST_CHANGES finding addressed:
- ✅ Silent failure on error response when
transportStartedthrowsTransferCancelledException— thereturnthat silently exitedimplPut()before reachinggenerateExceptionis replaced with a fall-through (empty catch), so the status check is always reached on both success and error responses.
All prior findings across 6 commits are now resolved. The fix is correct and well-layered:
- 32 KB chunked retroactive read (no
readAllBytes()memory spike) - Both
transportStartedandtransportProgressedare TCE-guarded in the retroactive path transportPropertiesAvailableis positioned before the status check — reachable on all response codes- The status check is always reached, even when TCE fires in the retroactive path
The one outstanding observation (no CI-deterministic test for the retroactive path, since macOS-specific) remains — but it was raised as a COMMENT in review 3, not a blocker, and is noted for future reference. LGTM.
This review was generated by an AI agent, Hermès on behalf of @gnodet.
Summary
Fixes the flaky
JettyTransporterTest.testPut_ProxyAuthenticatedandJettyTransporterTest.testPut_SSLtests that intermittently fail on macOS withprogressedCount = 0.Root Cause
JettyTransporter.implPut()relies on Jetty'sonRequestContentcallback to firetransportProgressedevents. On macOS runners with proxy authentication or TLS/HTTP2, Jetty's async pipeline can coalesce small request bodies (6 bytes in the test) with the headers send, bypassing the per-chunkonRequestContentcallback entirely. The upload succeeds (the file is written on the server), but the progress listener is never notified.This is a platform-specific timing issue in Jetty's HTTP/2 send pipeline — on Linux and Windows, the callback fires reliably; on macOS (kqueue-based IO), the write completes without the intermediate notification.
Fix
After a successful PUT request completes, check whether any
onRequestContentcallback actually fired (tracked via a newprogressedAtomicBoolean). If not, fire the missingtransportStarted/transportProgressedevents retroactively using the task's data. This is safe because:The fix is minimal (16 lines added) and does not change behavior when
onRequestContentfires normally — the retroactive path is only taken as a fallback.Testing
Hermes Agent (Claude Opus 4) on behalf of Guillaume Nodet