Skip to content
Open
Show file tree
Hide file tree
Changes from 6 commits
Commits
Show all changes
19 commits
Select commit Hold shift + click to select a range
5128066
feat(client): add REQUEST_BODY_FACTORY attribute key for reproducible…
yzfeng2020 Jul 1, 2026
c9d269d
feat(client): add non-buffering RequestFactoryHttpRequestDuplicator
yzfeng2020 Jul 1, 2026
2a846e7
feat(retry): use request body factory instead of buffering when attri…
yzfeng2020 Jul 1, 2026
d0d1d0d
feat(redirect): use request body factory instead of buffering when at…
yzfeng2020 Jul 1, 2026
4ca6f45
test(client): harden reproducible-body coverage; make firstDuplicateI…
yzfeng2020 Jul 1, 2026
c3928a1
refactor(client): redesign reproducible request bodies as a first-cla…
yzfeng2020 Jul 8, 2026
866dc10
refactor(client): collapse duplicate toDuplicator overloads in Reprod…
yzfeng2020 Jul 9, 2026
705aaf5
docs+test: address CodeRabbit review on reproducible request bodies
yzfeng2020 Jul 9, 2026
57ea25d
refactor(client): track all outstanding duplicates and guard abort races
yzfeng2020 Jul 15, 2026
80de365
docs+test: correct decorator ordering, cover multi-chunk bodies, tigh…
yzfeng2020 Jul 15, 2026
8860934
style: rename abort helper to satisfy OverloadMethodsDeclarationOrder
yzfeng2020 Jul 15, 2026
fa94f5c
fix(client): keep reproducible body across header rewrites; harden do…
yzfeng2020 Jul 16, 2026
f30c82c
refactor(common): move ReproducibleHttpRequestDuplicator into common,…
yzfeng2020 Aug 4, 2026
1286461
test(client): assert factory call count for direct consume, abort, an…
yzfeng2020 Aug 4, 2026
1f88945
test: cover abort-race teardown and redirect-hop factory failure; tig…
yzfeng2020 Aug 4, 2026
9592a79
test: add deterministic concurrent abort/duplicate race test; drop re…
yzfeng2020 Aug 4, 2026
8a66f3e
refactor: rename reproducible -> defer per review; scope content-leng…
yzfeng2020 Aug 10, 2026
0cd4d64
revert: restore `reproducible` naming per maintainer request; keep co…
yzfeng2020 Aug 11, 2026
7440a5e
test: deflake completedRequestIsUntracked by joining whenComplete bef…
yzfeng2020 Aug 11, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -185,9 +185,12 @@ private void execute0(ClientRequestContext ctx, RedirectContext redirectCtx,
return;
}

final HttpRequest duplicateReq = reqDuplicator.duplicate();
final HttpRequest duplicateReq;
final ClientRequestContext derivedCtx;
try {
// duplicate() may throw if the request body cannot be reproduced (see
// HttpRequest.reproducible); fail the request instead of following the redirect.
duplicateReq = reqDuplicator.duplicate();
derivedCtx = ClientUtil.newDerivedContext(ctx, duplicateReq, ctx.rpcRequest(), initialAttempt);
} catch (Throwable t) {
handleException(ctx, reqDuplicator, responseFuture, t, initialAttempt);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -296,16 +296,17 @@ private void doExecute0(ClientRequestContext ctx, HttpRequestDuplicator rootReqD
}

final HttpRequest duplicateReq;
if (initialAttempt) {
duplicateReq = rootReqDuplicator.duplicate();
} else {
final RequestHeadersBuilder newHeaders = originalReq.headers().toBuilder();
newHeaders.setInt(ARMERIA_RETRY_COUNT, totalAttempts - 1);
duplicateReq = rootReqDuplicator.duplicate(newHeaders.build());
}

final ClientRequestContext derivedCtx;
try {
// duplicate() may throw if the request body cannot be reproduced (see
// HttpRequest.reproducible); fail the request instead of retrying an unreproducible body.
if (initialAttempt) {
duplicateReq = rootReqDuplicator.duplicate();
} else {
final RequestHeadersBuilder newHeaders = originalReq.headers().toBuilder();
newHeaders.setInt(ARMERIA_RETRY_COUNT, totalAttempts - 1);
duplicateReq = rootReqDuplicator.duplicate(newHeaders.build());
}
derivedCtx = newDerivedContext(ctx, duplicateReq, ctx.rpcRequest(), initialAttempt);
} catch (Throwable t) {
handleException(ctx, rootReqDuplicator, future, t, initialAttempt);
Expand Down
49 changes: 49 additions & 0 deletions core/src/main/java/com/linecorp/armeria/common/HttpRequest.java
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,7 @@
import java.util.concurrent.CompletionStage;
import java.util.function.Consumer;
import java.util.function.Function;
import java.util.function.Supplier;

import org.reactivestreams.Publisher;
import org.reactivestreams.Subscriber;
Expand Down Expand Up @@ -357,6 +358,54 @@ static HttpRequest of(RequestHeaders headers,
return of(headers, StreamMessage.of(stage, subscriberExecutor));
}

/**
* Creates a new {@link HttpRequest} whose body can be reproduced on demand, so that
* {@code RetryingClient} and {@code RedirectingClient} can resend it without buffering the whole
* body in memory.
*
* <p>For streaming requests larger than about 2 GiB, buffering the body for replay is neither
* possible (an {@code int} size limit is exceeded) nor desirable. Instead, supply a factory that
* opens a fresh body {@link StreamMessage} on demand:
*
* <pre>{@code
* final RequestHeaders headers =
* RequestHeaders.of(HttpMethod.POST, "/upload",
* HttpHeaderNames.CONTENT_TYPE, "application/octet-stream");
* final HttpRequest req = HttpRequest.reproducible(headers, () -> StreamMessage.of(path));
Comment thread
coderabbitai[bot] marked this conversation as resolved.
* final RequestOptions options =
* RequestOptions.builder()
* .exchangeType(ExchangeType.REQUEST_STREAMING)
* .build();
* client.execute(req, options);
* }</pre>
*
* <p>The {@code bodyFactory} is invoked once per attempt (initial request, retry attempt, or
* redirect hop). The fixed {@code headers} are reused for every attempt; the factory regenerates
* only the body {@link StreamMessage}, so the request method and headers cannot drift between
* attempts. The body each factory invocation produces must match the declared
* {@link HttpHeaderNames#CONTENT_LENGTH} (if any), exactly as for any streaming
* {@link HttpRequest}.
*
* <p>Reproducible replay applies only to the outermost duplicating decorator (typically
* {@code RetryingClient}). Each attempt handed downstream is an ordinary {@link HttpRequest};
* an inner decorator (e.g. a redirect within a single retry attempt) treats it as a normal
* request and may buffer it. It is honored for streaming requests
* ({@link ExchangeType#isRequestStreaming()}); an aggregated exchange type buffers the body as
* usual.
*
* @param headers the fixed {@link RequestHeaders} reused for every attempt
* @param bodyFactory produces a fresh body {@link StreamMessage} for each attempt; it must not
* return {@code null}
*/
@UnstableApi
static HttpRequest reproducible(

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

So I understood this is combining two concerns:

  • Adding a static HttpRequest of(RequestHeaders, Supplier<StreamMessage>) variant
  • Adding a HttpRequest#withDuplicatorBodyFactory

e.g.

static HttpRequest of(RequestHeaders headers,
                      Supplier<? extends StreamMessage<? extends HttpObject>> bodyFactory) {
    requireNonNull(headers, "headers");
    requireNonNull(bodyFactory, "bodyFactory");
    return of(headers, bodyFactory.get());
}

...

static HttpRequest reproducible(
        RequestHeaders headers,
        Supplier<? extends StreamMessage<? extends HttpObject>> bodyFactory) {
    requireNonNull(headers, "headers");
    requireNonNull(bodyFactory, "bodyFactory");
    return of(headers, bodyFactory).withDuplicatorFactory(bodyFactory);
}

For your use-case, do you see most users will prefer using reproducible method directly rather than separately? If so, I think it's fine to add a convenience method, but I just wanted to double-check.

I do think the single factory method has value in that users only need to specify a single Supplier.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thanks — I'd prefer to keep the single reproducible(headers, factory) method rather than the two-method decomposition. The single-Supplier form makes the key invariant — the body source and the replay source are the same factory — unbreakable by construction. The decomposed shape (of(headers, Supplier) plus a separate withDuplicatorFactory(Supplier)) invites a footgun: two supplier references that can drift, or a forgotten withDuplicatorFactory silently falling back to the buffering (2 GiB-capped) path. Since this is @UnstableApi, we can add the composable primitives later if a concrete need arises, without committing to the wider surface now.

RequestHeaders headers,
Supplier<? extends StreamMessage<? extends HttpObject>> bodyFactory) {
Comment on lines +423 to +425

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Question) I'm unsure about the naming. What do you think of defer? It seems like the API looks similar to Flux#defer, and we'll likely be adding a StreamMessage#defer as well.

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@minwoox suggested that we keep the name reproducible since users may mistakenly think it may be safe to call subscribe HttpRequest#reproducible multiple times

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Good idea — renamed to defer. I've taken it all the way through: the public @UnstableApi method is now HttpRequest.defer(...), and the internal classes are DeferredHttpRequest / DeferredHttpRequestDuplicator (plus the test classes) so the vocabulary is consistent end to end. "Regenerate" stays as the verb for producing an equivalent body per attempt, since that describes the actual behavior where "defer" wouldn't fit. Pushed in 8a66f3e0e.

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Sorry but, could we revert to HttpRequest.reproducible(...)?

StreamMessage is single-subscription by contract, so the Flux.defer mental model (re-subscribe → regenerate) doesn't apply here — you can't re-subscribe. So defer names a property that isn't the point.

What actually defines this type is the opposite constraint: each call must supply an
equivalent StreamMessage (same bytes and trailers). reproducible names
that contract directly; defer only says "later" and says nothing about equivalence.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Agreed, and reverted to HttpRequest.reproducible(...) (pushed in 0cd4d64e8). You and @minwoox are right — since StreamMessage is single-subscription, the Flux#defer mental model doesn't carry over, and reproducible names the equivalent-body contract directly rather than just "later". The internal ReproducibleHttpRequest* classes and tests are back too. I kept the content-length doc scoping from the reverted commit (the framing warning now applies only when the caller sets content-length explicitly).

requireNonNull(headers, "headers");
requireNonNull(bodyFactory, "bodyFactory");
return new ReproducibleHttpRequest(headers, bodyFactory);
}

/**
* Returns a new {@link HttpRequestBuilder}.
*/
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,104 @@
/*
* Copyright 2026 LY Corporation
*
* LY Corporation licenses this file to you under the Apache License,
* version 2.0 (the "License"); you may not use this file except in compliance
* with the License. You may obtain a copy of the License at:
*
* https://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
* WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
* License for the specific language governing permissions and limitations
* under the License.
*/

package com.linecorp.armeria.common;

import java.util.concurrent.CompletableFuture;
import java.util.function.Supplier;

import org.reactivestreams.Publisher;

import com.linecorp.armeria.common.stream.StreamMessage;
import com.linecorp.armeria.internal.common.ReproducibleHttpRequestDuplicator;
import com.linecorp.armeria.internal.common.stream.NonOverridableStreamMessageWrapper;

import io.netty.util.concurrent.EventExecutor;

/**
* An {@link HttpRequest} whose body can be reproduced on demand, so {@code RetryingClient} and
* {@code RedirectingClient} can resend it without buffering the whole body in memory.
*
* <p>See {@link HttpRequest#reproducible(RequestHeaders, Supplier)} for details and usage.
*
* <p>The body factory is invoked lazily: when this request is duplicated (the retry/redirect path),
* {@link #toDuplicator(EventExecutor, long)} returns a {@link ReproducibleHttpRequestDuplicator} that
* calls the factory once per attempt and this request's own delegate is never subscribed. When this
* request is subscribed directly (no retry/redirect decorator), the factory is invoked exactly once,
* on subscription, to produce the single body. Either way the factory is never called eagerly.
*/
final class ReproducibleHttpRequest
extends NonOverridableStreamMessageWrapper<HttpObject, HttpRequestDuplicator> implements HttpRequest {

private final RequestHeaders headers;
private final Supplier<? extends StreamMessage<? extends HttpObject>> bodyFactory;

ReproducibleHttpRequest(RequestHeaders headers,
Supplier<? extends StreamMessage<? extends HttpObject>> bodyFactory) {
super(lazyBody(bodyFactory));
this.headers = headers;
this.bodyFactory = bodyFactory;
}

/**
* Returns a {@link StreamMessage} that invokes {@code bodyFactory} on its first (and only)
* subscription, so the factory is not called until this request is actually consumed directly.
*/
private static StreamMessage<HttpObject> lazyBody(
Supplier<? extends StreamMessage<? extends HttpObject>> bodyFactory) {
return StreamMessage.of((Publisher<HttpObject>) subscriber -> {

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Question) Is it necessary that the supplier isn't called eagerly? Asking because from what I understand, StreamMessage usually don't open resources until a downstream subscription is done.

If it is necessary for your use-case, I think it's fine to go ahead with this impl. I think it may be possible to generalize DeferredStreamMessage later on s.t. we can create a lazy supplier StreamMessage variant.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Laziness is necessary here: the caller's factory may have subscribe-time side effects (opening a file, acquiring a handle), and on the retry/redirect path the delegate is never subscribed — toDuplicator(...) drives the factory directly. An eager bodyFactory.get() at construction would materialize and leak a body on every reproducible request. The lazy wrapper confines materialization to the direct-consume path. Agreed that generalizing DeferredStreamMessage into a lazy-supplier StreamMessage variant would be cleaner and remove the dropped-SubscriptionOptions caveat — I'd like to do that as a follow-up rather than expand this PR.

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thanks

I'd like to do that as a follow-up rather than expand this PR.

Feel free to go ahead, or we can handle this as well if you are busy 🙇 Let us know

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

That'd be great — please go ahead with the DeferredStreamMessage generalization on your side. I'll keep the hand-rolled lazy body in this PR (its one caveat, dropped SubscriptionOptions, only affects the cold direct-consume path), and it can be swapped over to the generalized variant once yours lands. Thanks for offering to take it!

final StreamMessage<? extends HttpObject> body;
try {
body = bodyFactory.get();
} catch (Throwable t) {
StreamMessage.<HttpObject>aborted(t).subscribe(subscriber);
return;
}
if (body == null) {
StreamMessage.<HttpObject>aborted(
new NullPointerException("bodyFactory.get() returned null.")).subscribe(subscriber);
return;
}
@SuppressWarnings("unchecked")
final StreamMessage<HttpObject> cast = (StreamMessage<HttpObject>) body;
cast.subscribe(subscriber);
});
}

@Override
public RequestHeaders headers() {
return headers;
}

@SuppressWarnings("unchecked")
@Override
public CompletableFuture<AggregatedHttpRequest> aggregate(AggregationOptions options) {
return super.aggregate(options);
}

@Override
public HttpRequestDuplicator toDuplicator(EventExecutor executor) {
// Ignore the executor: the reproducible duplicator never buffers, so it needs no subscriber
// executor. Every duplicate() obtains a fresh body from the factory.
return new ReproducibleHttpRequestDuplicator(headers, bodyFactory);
}

@Override
public HttpRequestDuplicator toDuplicator(EventExecutor executor, long maxRequestLength) {
// maxRequestLength does not apply: this duplicator accumulates nothing, so there is no
// buffered length to cap. Each attempt streams a fresh body straight from the factory.
return new ReproducibleHttpRequestDuplicator(headers, bodyFactory);
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,130 @@
/*
* Copyright 2026 LY Corporation
*
* LY Corporation licenses this file to you under the Apache License,
* version 2.0 (the "License"); you may not use this file except in compliance
* with the License. You may obtain a copy of the License at:
*
* https://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
* WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
* License for the specific language governing permissions and limitations
* under the License.
*/

package com.linecorp.armeria.internal.common;

import static java.util.Objects.requireNonNull;

import java.util.function.Supplier;

import com.linecorp.armeria.common.HttpObject;
import com.linecorp.armeria.common.HttpRequest;
import com.linecorp.armeria.common.HttpRequestDuplicator;
import com.linecorp.armeria.common.RequestHeaders;
import com.linecorp.armeria.common.annotation.Nullable;
import com.linecorp.armeria.common.stream.StreamMessage;

/**
* An {@link HttpRequestDuplicator} that reproduces the request body without buffering it. Every
* {@link #duplicate()} — including the first — obtains a fresh body from the supplied factory, so no
* attempt reuses another attempt's stream and the original request handed to the client is never put
* on the wire. This avoids the ~2 GiB {@code int} size limit and the memory cost of
* {@code DefaultStreamMessageDuplicator}, which buffers the whole body for replay.
*
* <p>{@code RetryingClient} and {@code RedirectingClient} drive this duplicator strictly
* sequentially: at most one produced request is outstanding at a time, and each access

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I do think it is possible that abort on the original req can be called from any thread, which may result in a leak where the duplicated request isn't closed

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is handled by the instance lock, and I've now added tests that pin it. The race — abort(cause) firing after the factory produced a body but before duplicate() registers it — is closed under the lock: if abort wins, duplicate observes closed, tears its just-produced body down with the remembered cause, and throws; if duplicate wins, the child is in children and abort's snapshot catches it. New coverage: duplicateAfterAbortTearsDownProducedBodyWithCause (sequential) and concurrentAbortDuringDuplicateTearsDownProducedBody (a barrier-gated, genuinely multi-threaded interleave). I mutation-verified the latter is non-vacuous: removing the synchronized guards makes it fail (leaked body, no throw).

* happens-after the previous one via the async response future. The duplicator tracks the single
* last-produced request so that {@link #abort(Throwable)} can tear it down if it was produced but
* never subscribed (e.g. endpoint selection threw before the wire took ownership). The fields are
* {@code volatile} for safe publication across the caller-thread → event-loop hand-off, matching how
* the clients drive it.
*/
public final class ReproducibleHttpRequestDuplicator implements HttpRequestDuplicator {

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Question) Does this have to be in the internal package? Can this class be in the common pkg with ReproducibleHttpRequest?

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Good call — moved it to com.linecorp.armeria.common and made both the class and constructor package-private. It was only public to be reachable across the package boundary from ReproducibleHttpRequest#toDuplicator, and it has no external consumers, so this matches the sibling DefaultHttpRequestDuplicator exactly. Done in the latest push.


private final RequestHeaders headers;
private final Supplier<? extends StreamMessage<? extends HttpObject>> bodyFactory;

@Nullable
private volatile HttpRequest lastProduced;
private volatile boolean closed;

public ReproducibleHttpRequestDuplicator(
RequestHeaders headers,
Supplier<? extends StreamMessage<? extends HttpObject>> bodyFactory) {
this.headers = requireNonNull(headers, "headers");
this.bodyFactory = requireNonNull(bodyFactory, "bodyFactory");
}

@Override
public RequestHeaders headers() {
return headers;
}

@Override
public HttpRequest duplicate() {
return duplicate(headers);
}

@Override
public HttpRequest duplicate(RequestHeaders newHeaders) {
requireNonNull(newHeaders, "newHeaders");
if (closed) {
throw new IllegalStateException("duplicator is closed or aborted.");
}

// A produced-but-unsubscribed request from a previous attempt (e.g. endpoint selection threw
// before the wire subscribed) would otherwise be lost when we overwrite lastProduced; abort it
// so its body is released. Aborting an already-subscribed stream is a no-op.
final HttpRequest previous = lastProduced;

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think I prefer that the semantics of duplicate match that of the existing DefaultStreamMessageDuplicator in that duplicated streams are left to be active as long as the originating StreamMessage is not closed. (So a collection of child streams would be maintained, and closing the upstream will also close all child streams)

This would also help with supporting hedging more easily in the future.

Alternatively, I'm fine with keeping the current implementation and improving in the future as well as I do think complexity of the impl would go up slightly.

Let me know of your thoughts as well @minwoox @ikhoon

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

duplicated streams are left to be active as long as the originating StreamMessage is not closed.

Yeah, I think it's the contract of a duplicated request.

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

and closing the upstream will also close all child streams

If a factory creates a new child stream, it creates an independent stream, so each child may publish different elements. I don't think it will be used that way in most cases.

That said, given the limitation of the current approach - that the upstream and downstream data may differ - I'm not sure it makes sense to close the downstream when the upstream is closed.

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

If a factory creates a new child stream, it creates an independent stream, so each child may publish different elements.

The contract guarantees that it's the same stream:

* Every invocation must produce an <em>equivalent</em> body — the same bytes and
* trailers, not merely the same length — because the fixed {@code headers} (including any declared
* {@link HttpHeaderNames#CONTENT_LENGTH}) are reused verbatim on every attempt and are not

Unlike DefaultStreamMessageDuplicator, there is no single upstream. So children never share buffered signals and — as you note — could in principle publish different elements. We treat that as a caller contract (each factory invocation must produce an equivalent body matching the fixed content-length) rather than enforcing it, since the intended use is re-opening the same source, e.g. a file.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Summarizing where this landed: there's no shared upstream by design — each duplicate() produces an independent, equivalent body from the factory, which is exactly what avoids the 2 GiB buffer. So "closing the upstream closes children" doesn't map; there's nothing shared to propagate. abort() tears down all outstanding children to release unsubscribed bodies (e.g. open files), and close() leaves in-flight children streaming per the StreamMessageDuplicator contract, matching DefaultHttpRequestDuplicator. Concurrent children are already supported, so hedging is accommodated without the shared-upstream model. Keeping the current semantics.

if (previous != null) {
previous.abort();
}

// Fail fast on a broken factory: propagate to the caller (RetryingClient / RedirectingClient),
// which completes the response exceptionally instead of re-judging an aborted request and
// wasting the retry budget.
final StreamMessage<? extends HttpObject> body =
requireNonNull(bodyFactory.get(), "bodyFactory.get() returned null.");
final HttpRequest produced = HttpRequest.of(newHeaders, body);
lastProduced = produced;
return produced;
}

@Override
public void close() {
// Let the last-produced request finish: on the success path the wire owns it and is still
// streaming, and the StreamMessageDuplicator contract says close() must not abort issued
// duplicates. If it was never subscribed, it is completed/aborted by its own consumer or by a
// subsequent abort(); we intentionally do not abort it here.
closed = true;
}

@Override
public void abort() {
abortLastProduced(null);
}

@Override
public void abort(Throwable cause) {
abortLastProduced(requireNonNull(cause, "cause"));
}

private void abortLastProduced(@Nullable Throwable cause) {
closed = true;
final HttpRequest lastProduced = this.lastProduced;
if (lastProduced == null) {
return;
}
this.lastProduced = null;
// If the wire already owns the subscription, abort() on an already-subscribed stream is a
// no-op. Otherwise this releases the produced-but-unsubscribed body (e.g. an open file).
if (cause != null) {
lastProduced.abort(cause);
} else {
lastProduced.abort();
}
}
}
Loading
Loading