Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Fix response leak that can be caused by an exception during redirect #3095

Merged
merged 1 commit into from
Nov 11, 2024

Conversation

idelpivnitskiy
Copy link
Member

Motivation:

RedirectConfig accepts user-defined functions and it's possible that some of them can throw. RedirectSingle expects it and applies a try-catch block, but we forgot to drain the response payload body before propagating the error.

Modifications:

  • Drain response payload before propagating an exception during redirect processing;
  • Enhance tests to validate expected behavior;

Result:

We do not leave undrained response payload if redirect fails with an exception.

Motivation:

`RedirectConfig` accepts user-defined functions and it's possible that
some of them can throw. `RedirectSingle` expects it and applies a
`try-catch` block, but we forgot to drain the response payload body
before propagating the error.

Modifications:

- Drain response payload before propagating an exception during redirect
processing;
- Enhance tests to validate expected behavior;

Result:

We do not leave undrained response payload if redirect fails with an
exception.
bryce-anderson
bryce-anderson previously approved these changes Nov 9, 2024
// Drain response payload body before propagating the cause
sequentialCancellable.nextCancellable(response.messageBody().ignoreElements()
.whenOnError(suppressed -> safeOnError(target, addSuppressed(cause, suppressed)))
.subscribe(() -> safeOnError(target, cause)));
} else {
LOGGER.info("Ignoring exception from onSuccess of Subscriber {}.", target, cause);
Copy link
Contributor

Choose a reason for hiding this comment

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

In the terminalDelivered == true path we still don't drain and I see that we set that flag to true before we call many of the callbacks, eg

terminalDelivered = true;
target.onSuccess(response);

If it is target.onSuccess(..) that threw we will potentially still leak unless the onSuccess call did the draining. That does seem like it should be the responsibility of the onSucess method, but how defensive do we want to be?

Copy link
Member Author

Choose a reason for hiding this comment

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

The logic here is that we set the terminalDelivered = true and in the next line we subscribe to nextResponse single, which will first subscribe to response.messageBody().ignoreElements(), register its cancellable in sequentialCancellable, and only then will deal with target. This guarantees that we do not leak the response, it will either terminate or get canceled.

Copy link
Contributor

Choose a reason for hiding this comment

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

I think we may be talking about different cases as I'm talking about those higher up which you didn't modify. For example:

                final String location = redirectLocation(redirectCount, request, response);
                if (location == null) {
                    terminalDelivered = true;
                    target.onSuccess(response); // <- if I throw, is the response body drained or not?
                    return;
                }

There are a few others that follow the same pattern. Trying to determine if the body has been drained is perhaps too defensive but we also don't normally expect onSuccess(..) to throw so maybe it's better to aggressively drain?

Copy link
Contributor

Choose a reason for hiding this comment

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

Change otherwise LGTM, but I'd also like to understand how the flow works in the case that @bryce-anderson outlined.

Copy link
Member Author

Choose a reason for hiding this comment

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

Good point, if users have an unexpected exception inside an operator applied later (for example, Single.map(...)), there is a risk they can leak the response as well.
First, users code is outside of our responsibility. They should not throw from operators but propagate cancel/onError via reactive flow.
Second, as we discussed offline we can provide a general filter for all clients to handle this type of problem with the best effort. I will open a follow-up for that.

@bryce-anderson bryce-anderson dismissed their stale review November 9, 2024 16:16

Have a new question

Copy link
Contributor

@bryce-anderson bryce-anderson left a comment

Choose a reason for hiding this comment

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

I'm still curious what others think about draining if the target.onSuccess(..) calls throw, but that is subjective and this patch fixes an objective leak so I'm happy to 🚢 as is.

} catch (Throwable cause) {
if (!terminalDelivered) {
safeOnError(target, cause);
// Drain response payload body before propagating the cause
Copy link
Member

@Scottmitch Scottmitch Nov 11, 2024

Choose a reason for hiding this comment

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

I think I recall asking this previously (sorry if dup-question), but what if the payload body is very large or doesn't complete (malicious client, networking broken, etc.)? Will our timeouts kick-in at this level?

Copy link
Member Author

Choose a reason for hiding this comment

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

It depends on how users configure timeouts for their use cases. If they have it at the response payload body level, then yes.
We can add more protection as a separate work item and unify all places, bcz this is not the only place that drains payload.

Copy link
Member

Choose a reason for hiding this comment

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

followup PR sgtm

@idelpivnitskiy idelpivnitskiy merged commit eebe458 into apple:main Nov 11, 2024
11 checks passed
@idelpivnitskiy idelpivnitskiy deleted the redirect-leak branch November 11, 2024 19:07
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

4 participants