-
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
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -72,6 +72,10 @@ | |
import org.springframework.web.reactive.function.BodyInserter; | ||
import org.springframework.web.reactive.function.client.ClientRequest; | ||
import org.springframework.web.reactive.function.client.WebClient; | ||
import reactor.core.CoreSubscriber; | ||
import reactor.core.publisher.BaseSubscriber; | ||
import reactor.core.publisher.Mono; | ||
import reactor.util.context.Context; | ||
|
||
import java.net.URI; | ||
import java.time.Duration; | ||
|
@@ -84,6 +88,7 @@ | |
import java.util.function.Consumer; | ||
|
||
import static org.assertj.core.api.Assertions.assertThat; | ||
import static org.assertj.core.api.Assertions.assertThatCode; | ||
import static org.assertj.core.api.AssertionsForClassTypes.assertThatThrownBy; | ||
import static org.mockito.Mockito.*; | ||
import static org.springframework.http.HttpMethod.GET; | ||
|
@@ -144,9 +149,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 commentThe reason will be displayed to describe this comment to others. Learn more. Please revert the 2 changes in There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. No. |
||
SecurityContextHolder.clearContext(); | ||
RequestContextHolder.resetRequestAttributes(); | ||
this.function.destroy(); | ||
} | ||
|
||
@Test | ||
|
@@ -633,6 +639,90 @@ public void filterWhenRequestAttributesNotSetAndHooksInitHooksResetThenDefaultsN | |
assertThat(getBody(request)).isEmpty(); | ||
} | ||
|
||
// gh-7228 | ||
@Test | ||
public void afterPropertiesSetWhenHooksInitAndOutsideWebSecurityContextThenShouldNotThrowException() throws Exception { | ||
this.function.afterPropertiesSet(); // Hooks.onLastOperator() initialized | ||
assertThatCode(() -> Mono.subscriberContext().block()) | ||
.as("RequestContext Hook brakes application outside of web/security context") | ||
.doesNotThrowAnyException(); | ||
} | ||
|
||
@Test | ||
public void createRequestContextSubscriberIfNecessaryWhenOutsideWebSecurityContextThenReturnOriginalSubscriber() throws Exception { | ||
BaseSubscriber<Object> originalSubscriber = new BaseSubscriber<Object>() {}; | ||
CoreSubscriber<Object> resultSubscriber = this.function.createRequestContextSubscriberIfNecessary(originalSubscriber); | ||
assertThat(resultSubscriber).isSameAs(originalSubscriber); | ||
} | ||
|
||
// gh-7228 | ||
@Test | ||
public void createRequestContextSubscriberWhenRequestResponseProvidedThenCreateWithParentContext() throws Exception { | ||
testRequestContextSubscriber(new MockHttpServletRequest(), new MockHttpServletResponse(), null); | ||
} | ||
|
||
// gh-7228 | ||
@Test | ||
public void createRequestContextSubscriberWhenAuthenticationProvidedThenCreateWithParentContext() throws Exception { | ||
testRequestContextSubscriber(null, null, this.authentication); | ||
} | ||
|
||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Can you also add a test where if (parentContext.hasKey(REQUEST_CONTEXT_DATA_HOLDER)) {
context = parentContext;
`` There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. @jgrandja |
||
@Test | ||
public void createRequestContextSubscriberWhenParentContextHasDataHolderThenShouldReuseParentContext() throws Exception { | ||
RequestContextDataHolder testValue = new RequestContextDataHolder(null, null, null); | ||
final Context parentContext = Context.of(RequestContextSubscriber.REQUEST_CONTEXT_DATA_HOLDER, testValue); | ||
BaseSubscriber<Object> parent = new BaseSubscriber<Object>() { | ||
@Override | ||
public Context currentContext() { | ||
return parentContext; | ||
} | ||
}; | ||
|
||
RequestContextSubscriber<Object> requestContextSubscriber = | ||
new RequestContextSubscriber<>(parent, null, null, authentication); | ||
|
||
Context resultContext = requestContextSubscriber.currentContext(); | ||
|
||
assertThat(resultContext) | ||
.describedAs("parent context was replaced") | ||
.isSameAs(parentContext); | ||
} | ||
|
||
private void testRequestContextSubscriber(MockHttpServletRequest servletRequest, | ||
MockHttpServletResponse servletResponse, | ||
Authentication authentication) { | ||
String testKey = "test_key"; | ||
String testValue = "test_value"; | ||
|
||
BaseSubscriber<Object> parent = new BaseSubscriber<Object>() { | ||
@Override | ||
public Context currentContext() { | ||
return Context.of(testKey, testValue); | ||
} | ||
}; | ||
|
||
RequestContextSubscriber<Object> requestContextSubscriber = | ||
new RequestContextSubscriber<>(parent, servletRequest, servletResponse, authentication); | ||
|
||
Context resultContext = requestContextSubscriber.currentContext(); | ||
|
||
assertThat(resultContext) | ||
.describedAs("result context is null") | ||
.isNotNull(); | ||
|
||
assertThat(resultContext.getOrEmpty(testKey)) | ||
.describedAs("context is replaced") | ||
.hasValue(testValue); | ||
|
||
Object dataHolder = resultContext.getOrDefault(RequestContextSubscriber.REQUEST_CONTEXT_DATA_HOLDER, null); | ||
assertThat(dataHolder) | ||
.describedAs("context is not populated with REQUEST_CONTEXT_DATA_HOLDER") | ||
.isNotNull() | ||
.hasFieldOrPropertyWithValue("request", servletRequest) | ||
.hasFieldOrPropertyWithValue("response", servletResponse) | ||
.hasFieldOrPropertyWithValue("authentication", authentication); | ||
} | ||
|
||
private static String getBody(ClientRequest request) { | ||
final List<HttpMessageWriter<?>> messageWriters = new ArrayList<>(); | ||
messageWriters.add(new EncoderHttpMessageWriter<>(new ByteBufferEncoder())); | ||
|
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 returnnull
for these cases and ensureHooks.onLastOperator
is NOT called inafterPropertiesSet()
. 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 withnull
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:
null
values are propagated to the reactor context.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.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 sodelegate
subscriber looks fine to me as it means NOOP lift function.@jgrandja, do you still want to return
null
from lift function and createRequestContextSubscriber
in any case with emptyRequestContextDataHolder
?