From f2dd05da322e164825d39e60e2a38fabc9c0dcad Mon Sep 17 00:00:00 2001 From: Guillaume Nodet Date: Mon, 12 Oct 2026 19:23:16 +0000 Subject: [PATCH 1/6] Fix flaky JettyTransporterTest progress events on macOS 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 #2203 Co-authored-by: Claude Opus 4 --- .../aether/transport/jetty/JettyTransporter.java | 16 ++++++++++++++++ 1 file changed, 16 insertions(+) diff --git a/maven-resolver-transport-jetty/src/main/java/org/eclipse/aether/transport/jetty/JettyTransporter.java b/maven-resolver-transport-jetty/src/main/java/org/eclipse/aether/transport/jetty/JettyTransporter.java index e7db9cf25..13f8ec9d4 100644 --- a/maven-resolver-transport-jetty/src/main/java/org/eclipse/aether/transport/jetty/JettyTransporter.java +++ b/maven-resolver-transport-jetty/src/main/java/org/eclipse/aether/transport/jetty/JettyTransporter.java @@ -24,6 +24,7 @@ import java.io.InputStream; import java.net.URI; import java.net.URISyntaxException; +import java.nio.ByteBuffer; import java.nio.file.Files; import java.nio.file.Path; import java.nio.file.StandardCopyOption; @@ -374,6 +375,7 @@ protected void implPut(PutTask task) throws Exception { return true; // continue processing }); AtomicBoolean started = new AtomicBoolean(false); + AtomicBoolean progressed = new AtomicBoolean(false); Response response; try (InputStreamResponseListener listener = new InputStreamResponseListener()) { request.onRequestCommit(r -> { @@ -396,6 +398,7 @@ protected void implPut(PutTask task) throws Exception { return; } } + progressed.set(true); try { task.getListener().transportProgressed(b); } catch (TransferCancelledException e) { @@ -410,6 +413,19 @@ protected void implPut(PutTask task) throws Exception { throw new HttpTransporterException(statusCode); }); } + // On some platforms (macOS) with certain configurations (proxy auth, TLS/HTTP2), + // Jetty may complete a small PUT without firing onRequestContent callbacks, leaving + // the progress listener uninformed even though the upload succeeded. + // Fire the missing events now that we know the request completed successfully. + if (task.getDataLength() > 0 && !progressed.get()) { + if (started.compareAndSet(false, true)) { + task.getListener().transportStarted(0, task.getDataLength()); + } + try (InputStream is = task.newInputStream()) { + byte[] data = is.readAllBytes(); + task.getListener().transportProgressed(ByteBuffer.wrap(data)); + } + } } catch (ExecutionException e) { Throwable t = e.getCause(); if (t instanceof IOException ioex) { From e473f42038e6a759f6604dc09cd417da2c910c8b Mon Sep 17 00:00:00 2001 From: Guillaume Nodet Date: Mon, 12 Oct 2026 20:25:50 +0000 Subject: [PATCH 2/6] Use chunked read instead of readAllBytes() for retroactive progress events --- .../eclipse/aether/transport/jetty/JettyTransporter.java | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/maven-resolver-transport-jetty/src/main/java/org/eclipse/aether/transport/jetty/JettyTransporter.java b/maven-resolver-transport-jetty/src/main/java/org/eclipse/aether/transport/jetty/JettyTransporter.java index 13f8ec9d4..49b6cd8a5 100644 --- a/maven-resolver-transport-jetty/src/main/java/org/eclipse/aether/transport/jetty/JettyTransporter.java +++ b/maven-resolver-transport-jetty/src/main/java/org/eclipse/aether/transport/jetty/JettyTransporter.java @@ -422,8 +422,11 @@ protected void implPut(PutTask task) throws Exception { task.getListener().transportStarted(0, task.getDataLength()); } try (InputStream is = task.newInputStream()) { - byte[] data = is.readAllBytes(); - task.getListener().transportProgressed(ByteBuffer.wrap(data)); + byte[] buf = new byte[1024 * 32]; + int n; + while ((n = is.read(buf)) != -1) { + task.getListener().transportProgressed(ByteBuffer.wrap(buf, 0, n)); + } } } } catch (ExecutionException e) { From 49ca18d36aafcb54d1c1bd9540ec4a00df3cea3f Mon Sep 17 00:00:00 2001 From: Guillaume Nodet Date: Mon, 12 Oct 2026 20:39:45 +0000 Subject: [PATCH 3/6] Guard retroactive progress path against TransferCancelledException 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). --- .../aether/transport/jetty/JettyTransporter.java | 12 ++++++++++-- 1 file changed, 10 insertions(+), 2 deletions(-) diff --git a/maven-resolver-transport-jetty/src/main/java/org/eclipse/aether/transport/jetty/JettyTransporter.java b/maven-resolver-transport-jetty/src/main/java/org/eclipse/aether/transport/jetty/JettyTransporter.java index 49b6cd8a5..5d2c598aa 100644 --- a/maven-resolver-transport-jetty/src/main/java/org/eclipse/aether/transport/jetty/JettyTransporter.java +++ b/maven-resolver-transport-jetty/src/main/java/org/eclipse/aether/transport/jetty/JettyTransporter.java @@ -419,13 +419,21 @@ protected void implPut(PutTask task) throws Exception { // Fire the missing events now that we know the request completed successfully. if (task.getDataLength() > 0 && !progressed.get()) { if (started.compareAndSet(false, true)) { - task.getListener().transportStarted(0, task.getDataLength()); + try { + task.getListener().transportStarted(0, task.getDataLength()); + } catch (TransferCancelledException e) { + return; // upload already succeeded — retroactive notification cannot be cancelled + } } try (InputStream is = task.newInputStream()) { byte[] buf = new byte[1024 * 32]; int n; while ((n = is.read(buf)) != -1) { - task.getListener().transportProgressed(ByteBuffer.wrap(buf, 0, n)); + try { + task.getListener().transportProgressed(ByteBuffer.wrap(buf, 0, n)); + } catch (TransferCancelledException e) { + break; // upload already succeeded — retroactive notification cannot be cancelled + } } } } From 914a62126496767df3943d83444eedf1af00012d Mon Sep 17 00:00:00 2001 From: Guillaume Nodet Date: Mon, 12 Oct 2026 20:54:58 +0000 Subject: [PATCH 4/6] Fix transportPropertiesAvailable ordering to honour TransportListener contract --- .../eclipse/aether/transport/jetty/JettyTransporter.java | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/maven-resolver-transport-jetty/src/main/java/org/eclipse/aether/transport/jetty/JettyTransporter.java b/maven-resolver-transport-jetty/src/main/java/org/eclipse/aether/transport/jetty/JettyTransporter.java index 5d2c598aa..9cabf5501 100644 --- a/maven-resolver-transport-jetty/src/main/java/org/eclipse/aether/transport/jetty/JettyTransporter.java +++ b/maven-resolver-transport-jetty/src/main/java/org/eclipse/aether/transport/jetty/JettyTransporter.java @@ -407,7 +407,6 @@ protected void implPut(PutTask task) throws Exception { }) .send(listener); response = listener.get(requestTimeout, TimeUnit.MILLISECONDS); - task.getListener().transportPropertiesAvailable(createTransportProperties(request, rawResponseHeaders)); if (response.getStatus() >= MULTIPLE_CHOICES) { JettyRFC9457Reporter.INSTANCE.generateException(listener, (statusCode, reasonPhrase) -> { throw new HttpTransporterException(statusCode); @@ -437,6 +436,11 @@ protected void implPut(PutTask task) throws Exception { } } } + // transportPropertiesAvailable is called after transportStarted/transportProgressed + // to honour the TransportListener contract: "for remote put tasks this is called + // after transportStarted(long, long)". Moving it here ensures correct ordering in + // both the normal path and the retroactive-progress path. + task.getListener().transportPropertiesAvailable(createTransportProperties(request, rawResponseHeaders)); } catch (ExecutionException e) { Throwable t = e.getCause(); if (t instanceof IOException ioex) { From c00d3582621c62c72d0daf13544e1be8f76b90eb Mon Sep 17 00:00:00 2001 From: Guillaume Nodet Date: Mon, 12 Oct 2026 21:09:05 +0000 Subject: [PATCH 5/6] fix: restore transportPropertiesAvailable on error responses MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 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. --- .../transport/jetty/JettyTransporter.java | 23 ++++++++++--------- 1 file changed, 12 insertions(+), 11 deletions(-) diff --git a/maven-resolver-transport-jetty/src/main/java/org/eclipse/aether/transport/jetty/JettyTransporter.java b/maven-resolver-transport-jetty/src/main/java/org/eclipse/aether/transport/jetty/JettyTransporter.java index 9cabf5501..423bcdc51 100644 --- a/maven-resolver-transport-jetty/src/main/java/org/eclipse/aether/transport/jetty/JettyTransporter.java +++ b/maven-resolver-transport-jetty/src/main/java/org/eclipse/aether/transport/jetty/JettyTransporter.java @@ -407,21 +407,17 @@ protected void implPut(PutTask task) throws Exception { }) .send(listener); response = listener.get(requestTimeout, TimeUnit.MILLISECONDS); - if (response.getStatus() >= MULTIPLE_CHOICES) { - JettyRFC9457Reporter.INSTANCE.generateException(listener, (statusCode, reasonPhrase) -> { - throw new HttpTransporterException(statusCode); - }); - } // On some platforms (macOS) with certain configurations (proxy auth, TLS/HTTP2), // Jetty may complete a small PUT without firing onRequestContent callbacks, leaving - // the progress listener uninformed even though the upload succeeded. - // Fire the missing events now that we know the request completed successfully. + // the progress listener uninformed even though the upload was sent. + // Fire the missing events regardless of response status so the listener contract + // is honoured on both success and error paths. if (task.getDataLength() > 0 && !progressed.get()) { if (started.compareAndSet(false, true)) { try { task.getListener().transportStarted(0, task.getDataLength()); } catch (TransferCancelledException e) { - return; // upload already succeeded — retroactive notification cannot be cancelled + return; // upload already sent — retroactive notification cannot be cancelled } } try (InputStream is = task.newInputStream()) { @@ -431,16 +427,21 @@ protected void implPut(PutTask task) throws Exception { try { task.getListener().transportProgressed(ByteBuffer.wrap(buf, 0, n)); } catch (TransferCancelledException e) { - break; // upload already succeeded — retroactive notification cannot be cancelled + break; // upload already sent — retroactive notification cannot be cancelled } } } } // transportPropertiesAvailable is called after transportStarted/transportProgressed // to honour the TransportListener contract: "for remote put tasks this is called - // after transportStarted(long, long)". Moving it here ensures correct ordering in - // both the normal path and the retroactive-progress path. + // after transportStarted(long, long)". Placed before the status check so it is + // reachable on both success (2xx) and error (3xx/4xx/5xx) responses. task.getListener().transportPropertiesAvailable(createTransportProperties(request, rawResponseHeaders)); + if (response.getStatus() >= MULTIPLE_CHOICES) { + JettyRFC9457Reporter.INSTANCE.generateException(listener, (statusCode, reasonPhrase) -> { + throw new HttpTransporterException(statusCode); + }); + } } catch (ExecutionException e) { Throwable t = e.getCause(); if (t instanceof IOException ioex) { From 58810034ec2916dae0c009c64705ad30f09601d0 Mon Sep 17 00:00:00 2001 From: Guillaume Nodet Date: Mon, 12 Oct 2026 21:18:41 +0000 Subject: [PATCH 6/6] fix: fall through status check when TCE thrown in retroactive progress block MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 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. --- .../org/eclipse/aether/transport/jetty/JettyTransporter.java | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/maven-resolver-transport-jetty/src/main/java/org/eclipse/aether/transport/jetty/JettyTransporter.java b/maven-resolver-transport-jetty/src/main/java/org/eclipse/aether/transport/jetty/JettyTransporter.java index 423bcdc51..98e081c3e 100644 --- a/maven-resolver-transport-jetty/src/main/java/org/eclipse/aether/transport/jetty/JettyTransporter.java +++ b/maven-resolver-transport-jetty/src/main/java/org/eclipse/aether/transport/jetty/JettyTransporter.java @@ -417,7 +417,8 @@ protected void implPut(PutTask task) throws Exception { try { task.getListener().transportStarted(0, task.getDataLength()); } catch (TransferCancelledException e) { - return; // upload already sent — retroactive notification cannot be cancelled + // upload already sent — retroactive notification cannot be cancelled; + // fall through to transportPropertiesAvailable and status check } } try (InputStream is = task.newInputStream()) {