-
Notifications
You must be signed in to change notification settings - Fork 6.1k
Fix NPE in RequestContextSubscriber #7235
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
Conversation
@robotmrv Please sign the Contributor License Agreement! Click here to manually synchronize the status of this Pull Request. See the FAQ for frequently asked questions. |
@robotmrv Thank you for signing the Contributor License Agreement! |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Thanks for the PR! I added feedback inline
...t/web/reactive/function/client/ServletOAuth2AuthorizedClientExchangeFilterFunctionTests.java
Outdated
Show resolved
Hide resolved
I have doubts about |
@robotmrv - It looks like something needs to change to me as well. The ThreadLocal should only be invoked inside of defaultRequest which is guaranteed to be invoked on the original Servlet container Thread. I'm going to let @jgrandja take a look at this since he worked on some of the cleanup in this class. |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Thank you for these improvements @robotmrv! I've left some comments inline
...client/web/reactive/function/client/ServletOAuth2AuthorizedClientExchangeFilterFunction.java
Outdated
Show resolved
Hide resolved
...client/web/reactive/function/client/ServletOAuth2AuthorizedClientExchangeFilterFunction.java
Outdated
Show resolved
Hide resolved
...client/web/reactive/function/client/ServletOAuth2AuthorizedClientExchangeFilterFunction.java
Outdated
Show resolved
Hide resolved
...client/web/reactive/function/client/ServletOAuth2AuthorizedClientExchangeFilterFunction.java
Outdated
Show resolved
Hide resolved
...t/web/reactive/function/client/ServletOAuth2AuthorizedClientExchangeFilterFunctionTests.java
Outdated
Show resolved
Hide resolved
|
||
// gh-7228 | ||
@Test | ||
public void publisherWhenHooksInitAndSubscribedOutsideOfWebContextAndOutsideOfSecurityContextAndWithParentContextThenShouldNotReplaceParentContextAndShouldNotAddRequestContext() throws Exception { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I'm not sure about this test and the next one. It's quite low level and not directly testing the API of ServletOAuth2AuthorizedClientExchangeFilterFunction
. Most tests are directly testing filter()
and defaultRequest()
, whereas these 2 tests are directly using the Reactor API's.
The preference would be to change these tests so they are aligned with the existing filter()
tests.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The main issue was that current instrumentation affects all application
So this tests check if it is possible to performe operations on Publishers without web/sucurity context with initialized reactor instrumentation (which is ServletOAuth2AuthorizedClientExchangeFilterFunction functionality)
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Both of these tests do not directly test the API of ServletOAuth2AuthorizedClientExchangeFilterFunction
but instead are testing internal implementation details. In order to simplify the tests, please make RequestContextSubscriber
package-private along with the constructor. This will allow you to test the constructed state of RequestContextSubscriber
using currentContext()
. Please remove these 2 tests after you have added the new ones. Thanks.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
as this PR fixes scenario without web/security context (see #7235 (comment) ) and in this scenario ExchangeFilterFunction
API of ServletOAuth2AuthorizedClientExchangeFilterFunction
is not used
but ServletOAuth2AuthorizedClientExchangeFilterFunction#afterPropertiesSet
is used and it is part of API it is makes sense to test these scenarios without usage of ExchangeFilterFunction
part of API.
Other scenarios with direct usage of ExchangeFilterFunction
API are covered by existing tests.
I will make RequestContextSubscriber
package-private but test will not change very much as they could not use ExchangeFilterFunction
API by scenario.
These test could be moved to a new test class but I do not see point in this as they still test ServletOAuth2AuthorizedClientExchangeFilterFunction#afterPropertiesSet
API.
@jgrandja I've fixed codestyle according to convention standard and removed
|
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Thanks for the updates @robotmrv. I've left a couple comments inline
@@ -484,6 +494,9 @@ private ClientRequest bearer(ClientRequest request, OAuth2AuthorizedClient autho | |||
response = requestAttributes.getResponse(); | |||
} | |||
Authentication authentication = SecurityContextHolder.getContext().getAuthentication(); | |||
if (authentication == null && request == null && response == null) { | |||
return delegate; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This is misleading since it doesn't create and return a RequestContextSubscriber
instance, but instead returns the "current" Subscriber
. I think we should return null
for these cases and ensure Hooks.onLastOperator
is NOT called in afterPropertiesSet()
. Please add a test for this as well.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Hooks.onLastOperator() - applies global lift function on subscribe()/block()
and it should be better installed once per application life circle.
As Lift function is invoked only at the moment of subscribe()
lazily (not at the moment of application context / Filter initialization) it is not possible to unsure is there web/security context or not.
Not sure we are on the same page with case I try to fix
One of the cases:
I want to initialize application with some data. I have my reactive service API to do it.
I make ApplicationRunner
or just Application listener which invokes my reactive API on startup and call subscribe()/block().
So If my API implementation uses reactor context (e.g. reactive transactions, or just some custom implementation) with initialized hook from filter and without current fix I will get NPE, because RequestContextSubscriber
will populate reactor context with null
values (as there is no Web and Security Context) and this is forbidden.
Another scenario - my reactive API is invoked form RabbitMQ listener and there is no web context - as the result NPE.
So what PR fixes:
- it ensures that no
null
values are propagated to the reactor context. - As reactor context recommended to be as small as possible (see javadocs) this fix reduces number of used entries from 4 to 1/0.
RequestContextSubscriber
replaced original context so data from other hooks could be lost. This fix ensures thatRequestContextSubscriber
will not replace all parent context but adds values to it.- In described case
RequestContextDataHolder
will be created withnull
values and will be useless as it is not required at all for the operation (application initialization), so it is make sense not to populate reactor context with useless data because of p.2 and not to create additionalSubscriber
to reduce allocation and invocation stack size on each emit orcurrentContext()
Lift function can't return null
as later we will get NPE so delegate
subscriber looks fine to me as it means NOOP lift function.
@jgrandja, do you still want to return null
from lift function and create RequestContextSubscriber
in any case with empty RequestContextDataHolder
?
@@ -146,9 +153,10 @@ public void setup() { | |||
} | |||
|
|||
@After | |||
public void cleanup() { | |||
public void cleanup() throws Exception { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Please revert the 2 changes in cleanup()
as I believe it was added as a result of the removed test.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
No.
It removes reactor Hook from the filter, i.e. makes cleanup of the global state.
So it should be better here.
|
||
// gh-7228 | ||
@Test | ||
public void publisherWhenHooksInitAndSubscribedOutsideOfWebContextAndOutsideOfSecurityContextAndWithParentContextThenShouldNotReplaceParentContextAndShouldNotAddRequestContext() throws Exception { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Both of these tests do not directly test the API of ServletOAuth2AuthorizedClientExchangeFilterFunction
but instead are testing internal implementation details. In order to simplify the tests, please make RequestContextSubscriber
package-private along with the constructor. This will allow you to test the constructed state of RequestContextSubscriber
using currentContext()
. Please remove these 2 tests after you have added the new ones. Thanks.
@jgrandja , |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Thanks for the updates @robotmrv! I left a couple of other minor requests.
@@ -635,6 +641,69 @@ public void filterWhenRequestAttributesNotSetAndHooksInitHooksResetThenDefaultsN | |||
assertThat(getBody(request)).isEmpty(); | |||
} | |||
|
|||
// gh-7228 | |||
@Test | |||
public void afterPropertiesSetWhenHooksInitAndPublisherSubscribedOutsideOfWebContextAndOutsideOfSecurityContextThenSubscriptionShouldNotThrowException() throws Exception { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Let's shorten this name to afterPropertiesSetWhenHooksInitAndOutsideWebSecurityContextThenShouldNotThrowException
} | ||
|
||
@Test | ||
public void createRequestContextSubscriberIfNecessaryWhenOutsideOfWebContextAndOutsideOfSecurityContextThenShouldReturnOriginalSubscriber() throws Exception { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Please rename to createRequestContextSubscriberIfNecessaryWhenOutsideWebSecurityContextThenReturnOriginalSubscriber
|
||
// gh-7228 | ||
@Test | ||
public void requestSubscriberWhenHasRequestAndHasResponseThenShouldAddToParentContextRequestContextDataHolder() throws Exception { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Please rename to createRequestContextSubscriberWhenRequestResponseProvidedThenCreateWithParentContext
|
||
// gh-7228 | ||
@Test | ||
public void requestSubscriberWhenHasAuthenticationThenShouldAddToParentContextRequestContextDataHolder() throws Exception { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Please rename to createRequestContextSubscriberWhenAuthenticationProvidedThenCreateWithParentContext
public void requestSubscriberWhenHasAuthenticationThenShouldAddToParentContextRequestContextDataHolder() throws Exception { | ||
testRequestContextSubscriber(null, null, this.authentication); | ||
} | ||
|
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Can you also add a test where RequestContextSubscriber
constructor uses parentContext
if (parentContext.hasKey(REQUEST_CONTEXT_DATA_HOLDER)) {
context = parentContext;
``
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
@jgrandja
I've added test and renamed existing as you suggested.
please check
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Thanks @robotmrv. I left one minor request. After you apply the update can you please rebase off master and squash commits. Thank you.
testRequestContextSubscriber(null, null, this.authentication); | ||
} | ||
|
||
@Test | ||
public void createRequestContextSubscriberWhenParentContextHasDataHolderThenShouldReuseParentContext() throws Exception { | ||
RequestContextDataHolder testValue = mock(RequestContextDataHolder.class); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Instead of a mock, we can just set an empty RequestContextDataHolder
instance
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
@jgrandja done
RequestContextSubscriber could cause NPE if Mono/Flux.subscribe() was invoked outside of Web Context. In addition it replaced source Context with its own without respect to old data. Now Request Context Data is Propagated within holder class and it is added to existing reactor Context if Holder is not empty. Fixes spring-projectsgh-7228
Thank you for all the updates @robotmrv. This is now in master. |
RequestContextSubscriber could cause NPE if Mono/Flux.subscribe
was invoked outside of Web Context.
In addition it replaced source Context with its own without respect
to old data.
Now Request Context Data is Propagated within holder class and
it is added to existing reactor Context
Fixes gh-7228