Subscriber context not found in doOnError - spring-boot

I was wondering if someone could eyeball the following code snippet and tell me why the SubscriberContext inside the doOnError is not trigerred
public Mono<ServerResponse> handlePlatformAuthenticationResponse(final ServerRequest serverRequest) {
Mono<MultiValueMap<String, String>> formData = serverRequest.body(BodyExtractors.toFormData());
return formData
.flatMap(this::provisionUserAndClass)
.flatMap(tuple -> Mono.subscriberContext()
.map(context -> {
// this is invoked if provisionUserAndClass completes successfully
TelemetryData telemetryData = context.get(TELEMETRY_DATA);
LTILaunchRequest<LTILaunchRequestSettings> launchRequest = tuple.getT2();
this.addLaunchDetailsToTelemetryContext(launchRequest, telemetryData);
return tuple;
}))
.doOnError(error -> Mono.subscriberContext()
.map(context -> {
// this is never invoked if provisionUserAndClass returns a Mono.error
TelemetryData telemetryData = context.get(TELEMETRY_DATA);
// log telemetryData + error message
}))
.subscriberContext(context -> context.put(TELEMETRY_DATA, new TelemetryData()));
}
private Mono<Tuple2<ClassAndUserProvisioningResponse, LTILaunchRequest<LTILaunchRequestSettings>>> provisionUserAndClass(
LTILaunchRequest<LTILaunchRequestSettings> ltiLaunchRequest) {
// returning a Mono.error just to see behavior of Mono.subscriberContext() when error occurs. Actual code will call a service method
return Mono.error(new ProvisioningException("fake"));
}

To access context in case of error you could use doOnEach operator:
.doOnEach(signal -> {
if (signal.isOnError())
{
TelemetryData telemetryData = signal.getContext().get(TELEMETRY_DATA);
Throwable error = signal.getThrowable();
// ...
}
})
Mono.subscriberContext() can only be used meaningfully in operators where you have to return a Mono, like flatMap, concatMap, etc, but not in side-effect operators where there is nothing that would subscribe to the Mono<Context>.

.doOnError(error -> Mono.subscriberContext()
.map(context -> {
// this is never invoked if provisionUserAndClass returns a Mono.error
TelemetryData telemetryData = context.get(TELEMETRY_DATA);
// log telemetryData + error message
}).subscribe())
You forgot to subscribe to the Mono.subscriberContext().

Related

Spring reactive WebSocketHandler mono does not reach doFinally block

I am trying to handle closing web socket session in WebSocketHandler. My intuition was to do it in this way:
webSocketClient.execute(
URI.create("some-ws-endpoint")
) { session: WebSocketSession ->
session.receive()
.doOnEach { action(it) }
.then()
.doFinally { session.close() }
}
but I cannot reach doFinally block from Mono<Void> returned by webSocketClient.execute. My full test code for this case is:
fun test() = runBlocking {
val webSocketClient: WebSocketClient = StandardWebSocketClient()
val subscription = webSocketClient.execute(
URI.create("some-ws-endpoint")
) { session: WebSocketSession ->
session.receive()
.doOnEach { println("Message: $it") }
.then()
.doFinally { println("finally") }
}.subscribe()
delay(20000)
subscription.dispose()
delay(5000)
}
from which I have Messages printed, but finally is never shown on my console. From the other hand when I tried to do it on plain reactor-core components, everything works just fine:
runBlocking {
val publisher: Flux<Long> = Flux.interval(Duration.ofSeconds(1))
val subscription = publisher
.doOnEach { println("Value: $it") }
.then()
.doFinally { println("in doFinally") }
.subscribe()
delay(5_000)
subscription.dispose()
delay(1_000)
}
I am new to both WebSockets and Project Reactor, so maybe I am doing some basic mistake. Does anyone see what is wrong with my code?

how to render an object with two flux fields without blocking?

I want to render an object composed of two mono or flux elements (below a code snippet):
Mono<List<NodeDTO>> nodeDTOFlux = this.webClient
.get()
.uri(NODES_WITH_LIMIT + limit)
.retrieve()
.onStatus(HttpStatus::isError,
response -> response.bodyToMono(String.class).flatMap(
msg -> Mono.error(new ApiCallException(msg, response.statusCode())))
)
.bodyToFlux(new ParameterizedTypeReference<Node>() {
}).map(node -> nodeMapper.toNodeDTO(node))
.collectList();
Mono<List<EdgeDTO>> edgeDTOFlux = this.webClient
.get()
.uri(EDGES_WITH_LIMIT + limit)
.retrieve()
.onStatus(HttpStatus::isError,
response -> response.bodyToMono(String.class).flatMap(
msg -> Mono.error(new ApiCallException(msg, response.statusCode())))
)
.bodyToFlux(new ParameterizedTypeReference<Edge>() {
}).map(edge -> edgeMapper.toEdgeDTO(edge))
.collectList();
I tried with zip() method but it's not what I aim to do
I tried to return an object like this
GraphDataDTO graphDataDTO = new GraphDataDTO();
graphDataDTO.setEdgeDTOS(edgeDTOFlux);
graphDataDTO.setNodeDTOS(nodeDTOFlux);
I have a result in my console but the object returned
{
"nodeDTOS": {
"scanAvailable": true
},
"edgeDTOS": {
"scanAvailable": true
}
}
the return is done before getting all the flux.. is there any solution without blocking !
thanks in advance.
This should work:
return Mono.zip(nodeDTOFlux, edgeDTOFlux)
.map(tuple2 -> GraphDataDTO.builder().nodeDTO(tuple2.getT1()).edgeDTO(tuple2.getT2()).build())
It creates a Tuple of NodeDTO and EdgeDTO and maps it into GraphDataDTO.

What's the best pattern for exception handling when using coroutines in kotlinjs?

I have a kotlinjs app. I handle a particular event (dropping of data onto a component) like this:
onEvent {
drop = { event ->
GlobalScope.async {
//...
dropTask(y, data)
}
}
}
// ...
// this function has to be a suspend function because model's is
private suspend fun dropTask(y: Int, taskId: TaskId) {
// ... prepare data
model.insertBefore(taskId!!, insertBefore?.id)
}
// ... Model's function is defined like this:
suspend fun insertBefore(taskToInsert: TaskId, taskBefore: TaskId?) {
val (src, _) = memory.find(taskToInsert)
// ... and finally, the find function is:
fun find(taskId: TaskId): Pair<Task?, Int> {
// ...
return if (task != null) {
// ...
} else {
throw Exception("Couldn't find task with id $taskId!!")
}
}
The issue is that the Exception gets thrown, but isn't reported anywhere.
I have tried:
a) Installing a CoroutineExceptionHandler into the GlobalScope.async (i.e.:
val handler = CoroutineExceptionHandler { _, e ->
console.log("Caught exception: $e")
}
GlobalScope.async(handler) {
...but this never gets called. This would be relatively clean if I could make it work. It would be even nicer if this was default behavior for kotlinjs, so that exceptions weren't accidentally unreported.
b) Calling await:
drop = { event ->
GlobalScope.launch {
GlobalScope.async() {
// ...
dropTask(y, data)
}.await()
}
}
This does result in the exception being logged to the console, but it's so ugly. It's not possible to call .await() outside of a suspend function or coroutine, so for this particular event handler I have to wrap the async call in a launch. I must be doing something wrong. Anybody have a better pattern that I should be using?

RxSwift form validation and sending request in one stream

I have a case where I would like to validate form and then if everything is ok go to api request.
I've written some code and it works fine but errors dispose my stream. I know I could add .catch error at the end of flat map but then next flat map would be executed.
Can I add catch error at the end of stream without disposing it? Or the only way to deal with it is separate it to two streams validation and server responses?
enum Response {
case error(message: String)
case success
}
let start = input.validate
.withLatestFrom(input.textFields)
.flatMap { [unowned self] fields -> Observable<String> in
return self.validate(characters: fields)
}
.flatMapLatest { [unowned self] code -> Observable<String> in
return self.apiClient.rxSendData(code)
.retry(1)
}
.map { _ in return Response.success }
.asDriver { Driver.just(Response.error(message: $0.localizedDescription)) }
I'm making some assumptions about code you aren't showing. Your validate function is especially odd to me. It looks like it emits a String (which is ignored, if validation was successful and doesn't emit anything (or maybe an error) if validation failed?
let start = input.validate
.withLatestFrom(input.textFields)
.flatMapLatest { [unowned self] fields -> Observable<String> in
return self.validate(characters: fields)
.catchError { _ in Observable.empty() } // empty() doesn't emit a value so the next flatMap won't be executed.
}
.flatMapLatest { [unowned self] _ -> Observable<Response> in
return self.apiClient.rxSendData()
.retry(1)
.map { _ in Response.success }
.catchError { error in Observable.just(Response.error(message: error.localizedDescription)) }
}
.asDriver { Driver.just(Response.error(message: $0.localizedDescription)) }
If validate emits an error when validation fails, and you want to capture that error, then something like this would work:
let start = input.validate
.withLatestFrom(input.textFields)
.flatMapLatest { [unowned self] fields -> Observable<Response> in
return self.validate(characters: fields)
.map { _ in Response.success }
.catchError { Observable.just(Response.error(message: $0.localizedDescription)) }
}
.flatMapLatest { [unowned self] validation -> Observable<Response> in
// here, the above flatMap emits a value no matter what, so we have to switch on it to determine if we want to continue or just push the Response down the pipe.
switch validation {
case .error:
return Observable.just(validation)
case .success:
return self.apiClient.rxSendData()
.retry(1)
.map { _ in Response.success }
.catchError { error in Observable.just(Response.error(message: error.localizedDescription)) }
}
}
.asDriver { Driver.just(Response.error(message: $0.localizedDescription)) }
Have you considered the materialize operator? It converts an observable sequence into an observable sequence of event objects detailing what happened that cannot error but completes when the input sequence completes. You can then share that.
Something like:
let code = input.validate
.withLatestFrom(input.textFields)
.flatMap { [unowned self] fields -> Observable<String> in
self.validate(characters: fields)
.materialize()
}
.share(replay: 1)
code
.compactMap { $0.error }
.subscribe() // Show error from `self.validate`
.disposed(by: bag)
let request = code
.compactMap { $0.element }
// Will get to this flat map only if `self.validate` did not error
.flatMapLatest { [unowned self] code -> Observable<String> in
self.apiClient.rxSendData(code)
.retry(1)
.materialize()
}
.share(replay: 1)
request
.compactMap { $0.error }
.subscribe() // Show error from `self.apiClient.rxSendData`
.disposed(by: bag)
request
.compactMap { $0.element }
// Do something as a result of the request being successful
The chains would not cease upon self.validate and self.apiClient.rxSendData emitting errors.

Cannot specialize non-generic type ResponseSerializer

From the documentation and the 2.0 Migrate Guide I tried to use Response Serialization but I'm having the following errors. I can't seem to figure out the problems. I'm also having the same errors with ResponseCollectionSerializable.
You should use GenericResponseSerializer that conforms to ResponseSerializer:
public protocol ResponseObjectSerializable {
init?(response: NSHTTPURLResponse, representation: AnyObject)
}
extension Request {
public func responseObject<T: ResponseObjectSerializable>(completionHandler: (NSURLRequest?, NSHTTPURLResponse?, Result<T>) -> Void) -> Self {
let responseSerializer = GenericResponseSerializer<T> { request, response, data in
let JSONResponseSerializer = Request.JSONResponseSerializer(options: .AllowFragments)
let result = JSONResponseSerializer.serializeResponse(request, response, data)
switch result {
case .Success(let value):
if let
response = response,
responseObject = T(response: response, representation: value)
{
return .Success(responseObject)
} else {
let failureReason = "JSON could not be serialized into response object: \(value)"
let error = Error.errorWithCode(.JSONSerializationFailed, failureReason: failureReason)
return .Failure(data, error)
}
case .Failure(let data, let error):
return .Failure(data, error)
}
}
return response(responseSerializer: responseSerializer, completionHandler: completionHandler)
}
}
ResponseSerializer is the protocol in which all response serializers must conform to.

Resources