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

WebSockets Next: error handlers part 4 #39852

Merged
merged 1 commit into from
Apr 4, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
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
52 changes: 41 additions & 11 deletions docs/src/main/asciidoc/websockets-next-reference.adoc
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,8 @@
and pull requests should be submitted there:
https://github.com/quarkusio/quarkus/tree/main/docs/src/main/asciidoc
////
= WebSockets-Next extension reference guide
[id="websockets-next-reference-guide"]
= WebSockets Next extension reference guide
:extension-status: preview
include::_attributes.adoc[]
:numbered:
Expand All @@ -12,7 +13,8 @@
:topics: web,websockets
:extensions: io.quarkus:quarkus-websockets-next

IMPORTANT: The `websockets-next` extension is experimental. The proposal API may change in future releases.
The `websockets-next` extension provides an experimental API to define _WebSocket_ endpoints declaratively.
The proposed API may change in future releases.

Check warning on line 17 in docs/src/main/asciidoc/websockets-next-reference.adoc

View workflow job for this annotation

GitHub Actions / Linting with Vale

[vale] reported by reviewdog 🐶 [Quarkus.TermsWarnings] Consider using 'might (for possiblity)' or 'can (for ability)' rather than 'may' unless updating existing content that uses the term. Raw Output: {"message": "[Quarkus.TermsWarnings] Consider using 'might (for possiblity)' or 'can (for ability)' rather than 'may' unless updating existing content that uses the term.", "location": {"path": "docs/src/main/asciidoc/websockets-next-reference.adoc", "range": {"start": {"line": 17, "column": 18}}}, "severity": "WARNING"}

== The WebSocket protocol

Expand Down Expand Up @@ -193,8 +195,9 @@
* At most one `@OnBinaryMessage` method: Handles the binary messages the connected client sends.
* At most one `@OnOpen` method: Invoked when a client connects to the WebSocket.
* At most one `@OnClose` method: Executed upon the client disconnecting from the WebSocket.
* Any number of `@OnError` methods: Invoked when an error occurs; that is when an endpoint callback throws a runtime error, or when a conversion errors occurs, or when a returned `io.smallrye.mutiny.Uni`/`io.smallrye.mutiny.Multi` receives a failure.

Only some endpoints need to include all methods.

Check warning on line 200 in docs/src/main/asciidoc/websockets-next-reference.adoc

View workflow job for this annotation

GitHub Actions / Linting with Vale

[vale] reported by reviewdog 🐶 [Quarkus.Fluff] Depending on the context, consider using 'Rewrite the sentence, or use 'must', instead of' rather than 'need to'. Raw Output: {"message": "[Quarkus.Fluff] Depending on the context, consider using 'Rewrite the sentence, or use 'must', instead of' rather than 'need to'.", "location": {"path": "docs/src/main/asciidoc/websockets-next-reference.adoc", "range": {"start": {"line": 200, "column": 10}}}, "severity": "INFO"}
However, it must contain at least `@On[Text|Binary]Message` or `@OnOpen`.

An error is thrown at build time if any endpoint violates these rules.
Expand Down Expand Up @@ -222,23 +225,26 @@
* Methods annotated with `@RunOnVirtualThread` are considered blocking and should execute on a virtual thread.
* Blocking methods must execute on a worker thread if not annotated with `@RunOnVirtualThread`.
* When `@RunOnVirtualThread` is employed, each invocation spawns a new virtual thread.
* Methods returning `CompletionStage` and `Uni` are considered non-blocking
* Methods returning `Multi` are considered non-blocking and must be subscribed to, except if they return their own `Multi`.
* Methods returning `CompletionStage`, `Uni` and `Multi` are considered non-blocking.
* Methods returning `void` or plain objects are considered blocking.

=== Parameters
=== Method Parameters

These methods can accept parameters in two formats:
The method must accept exactly one message parameter:

* The message object (of any type).
* A `Multi<X>` with X as the message type.
* Any other parameters should be flagged as errors.

However, it may also accept the following parameters:

Check warning on line 238 in docs/src/main/asciidoc/websockets-next-reference.adoc

View workflow job for this annotation

GitHub Actions / Linting with Vale

[vale] reported by reviewdog 🐶 [Quarkus.TermsWarnings] Consider using 'might (for possiblity)' or 'can (for ability)' rather than 'may' unless updating existing content that uses the term. Raw Output: {"message": "[Quarkus.TermsWarnings] Consider using 'might (for possiblity)' or 'can (for ability)' rather than 'may' unless updating existing content that uses the term.", "location": {"path": "docs/src/main/asciidoc/websockets-next-reference.adoc", "range": {"start": {"line": 238, "column": 13}}}, "severity": "WARNING"}

* `WebSocketConnection`
* `HandshakeRequest`
* `String` parameters annotated with `@PathParam`

The message object represents the data sent and can be accessed as either raw content (`String`, `JsonObject`, `JsonArray`, `Buffer` or `byte[]`) or deserialized high-level objects, which is the recommended approach.

Check warning on line 244 in docs/src/main/asciidoc/websockets-next-reference.adoc

View workflow job for this annotation

GitHub Actions / Linting with Vale

[vale] reported by reviewdog 🐶 [Quarkus.TermsSuggestions] Depending on the context, consider using 'because' or 'while' rather than 'as'. Raw Output: {"message": "[Quarkus.TermsSuggestions] Depending on the context, consider using 'because' or 'while' rather than 'as'.", "location": {"path": "docs/src/main/asciidoc/websockets-next-reference.adoc", "range": {"start": {"line": 244, "column": 65}}}, "severity": "INFO"}

When receiving a `Multi`, the method is invoked once per connection, and the provided `Multi` receives the items transmitted by this connection.
The method must subscribe to the `Multi` to receive these items (or return a Multi).
Cancelling this subscription closes the associated connection.

=== Allowed Returned Types

Expand Down Expand Up @@ -324,8 +330,8 @@
Emitting `null` signifies no response to be sent to the client, allowing for skipping a response when needed.

=== JsonObject and JsonArray
Vert.x `JSONObject` and `JSONArray` instances bypass the serialization and deserialization mechanisms.
Vert.x `JsonObject` and `JsonArray` instances bypass the serialization and deserialization mechanisms.
Messages are sent as text messages.

Check warning on line 334 in docs/src/main/asciidoc/websockets-next-reference.adoc

View workflow job for this annotation

GitHub Actions / Linting with Vale

[vale] reported by reviewdog 🐶 [Quarkus.TermsSuggestions] Depending on the context, consider using 'because' or 'while' rather than 'as'. Raw Output: {"message": "[Quarkus.TermsSuggestions] Depending on the context, consider using 'because' or 'while' rather than 'as'.", "location": {"path": "docs/src/main/asciidoc/websockets-next-reference.adoc", "range": {"start": {"line": 334, "column": 8}}}, "severity": "INFO"}

Check warning on line 334 in docs/src/main/asciidoc/websockets-next-reference.adoc

View workflow job for this annotation

GitHub Actions / Linting with Vale

[vale] reported by reviewdog 🐶 [Quarkus.HeadingPunctuation] Do not use end punctuation in headings. Raw Output: {"message": "[Quarkus.HeadingPunctuation] Do not use end punctuation in headings.", "location": {"path": "docs/src/main/asciidoc/websockets-next-reference.adoc", "range": {"start": {"line": 334, "column": 27}}}, "severity": "INFO"}

=== Broadcasting
By default, responses produced by `@On[Text|Binary]Message` methods are sent back to the connected client.
Expand All @@ -341,6 +347,8 @@

The same principle applies to methods returning instances of `Multi` or `Uni`.

NOTE: If you need to select the connected clients that should receive the message, you can use `WebSocketConnection.broadcast().filter().sendText()`.

== OnOpen and OnClose methods

The WebSocket endpoint can also be notified when a client connects or disconnects.
Expand Down Expand Up @@ -369,8 +377,11 @@

=== Parameters

Methods annotated with `@OnOpen` and `@OnClose` do not accept any parameters.
If such methods declare parameters, they will be flagged as errors and reported at build time.
Methods annotated with `@OnOpen` and `@OnClose` may accept the following parameters:

* `WebSocketConnection`
* `HandshakeRequest`
* `String` parameters annotated with `@PathParam`

=== Allowed Returned Types

Expand Down Expand Up @@ -425,6 +436,25 @@
}
----

== Error Handling

The WebSocket endpoint can also be notified when an error occurs.
A WebSocket endpoint method annotated with `@io.quarkus.websockets.next.OnError` is invoked when an endpoint callback throws a runtime error, or when a conversion errors occurs,
or when a returned `io.smallrye.mutiny.Uni`/`io.smallrye.mutiny.Multi` receives a failure.

The method must accept exactly one "error" parameter, i.e. a parameter that is assignable from `java.lang.Throwable`.

Check failure on line 445 in docs/src/main/asciidoc/websockets-next-reference.adoc

View workflow job for this annotation

GitHub Actions / Linting with Vale

[vale] reported by reviewdog 🐶 [Quarkus.TermsErrors] Use 'you' rather than 'i'. Raw Output: {"message": "[Quarkus.TermsErrors] Use 'you' rather than 'i'.", "location": {"path": "docs/src/main/asciidoc/websockets-next-reference.adoc", "range": {"start": {"line": 445, "column": 44}}}, "severity": "ERROR"}

Check warning on line 445 in docs/src/main/asciidoc/websockets-next-reference.adoc

View workflow job for this annotation

GitHub Actions / Linting with Vale

[vale] reported by reviewdog 🐶 [Quarkus.TermsWarnings] Consider using 'that is' rather than 'i.e.' unless updating existing content that uses the term. Raw Output: {"message": "[Quarkus.TermsWarnings] Consider using 'that is' rather than 'i.e.' unless updating existing content that uses the term.", "location": {"path": "docs/src/main/asciidoc/websockets-next-reference.adoc", "range": {"start": {"line": 445, "column": 44}}}, "severity": "WARNING"}
The method may also accept the following parameters:

Check warning on line 446 in docs/src/main/asciidoc/websockets-next-reference.adoc

View workflow job for this annotation

GitHub Actions / Linting with Vale

[vale] reported by reviewdog 🐶 [Quarkus.TermsWarnings] Consider using 'might (for possiblity)' or 'can (for ability)' rather than 'may' unless updating existing content that uses the term. Raw Output: {"message": "[Quarkus.TermsWarnings] Consider using 'might (for possiblity)' or 'can (for ability)' rather than 'may' unless updating existing content that uses the term.", "location": {"path": "docs/src/main/asciidoc/websockets-next-reference.adoc", "range": {"start": {"line": 446, "column": 1}}}, "severity": "WARNING"}

* `WebSocketConnection`
* `HandshakeRequest`
* `String` parameters annotated with `@PathParam`

An endpoint may declare multiple methods annotated with `@io.quarkus.websockets.next.OnError`.

Check warning on line 452 in docs/src/main/asciidoc/websockets-next-reference.adoc

View workflow job for this annotation

GitHub Actions / Linting with Vale

[vale] reported by reviewdog 🐶 [Quarkus.TermsWarnings] Consider using 'might (for possiblity)' or 'can (for ability)' rather than 'may' unless updating existing content that uses the term. Raw Output: {"message": "[Quarkus.TermsWarnings] Consider using 'might (for possiblity)' or 'can (for ability)' rather than 'may' unless updating existing content that uses the term.", "location": {"path": "docs/src/main/asciidoc/websockets-next-reference.adoc", "range": {"start": {"line": 452, "column": 2}}}, "severity": "WARNING"}
However, each method must declare a different error parameter.
The method that declares a most-specific supertype of the actual exception is selected.

NOTE: The `@io.quarkus.websockets.next.OnError` annotation can be also used to declare a global error handler, i.e. a method that is not declared on a WebSocket endpoint. Such a method may not accept `@PathParam` paremeters. Error handlers declared on an endpoint take precedence over the global error handlers.

Check warning on line 456 in docs/src/main/asciidoc/websockets-next-reference.adoc

View workflow job for this annotation

GitHub Actions / Linting with Vale

[vale] reported by reviewdog 🐶 [Quarkus.TermsWarnings] Consider using 'that is' rather than 'i.e.' unless updating existing content that uses the term. Raw Output: {"message": "[Quarkus.TermsWarnings] Consider using 'that is' rather than 'i.e.' unless updating existing content that uses the term.", "location": {"path": "docs/src/main/asciidoc/websockets-next-reference.adoc", "range": {"start": {"line": 456, "column": 101}}}, "severity": "WARNING"}

== Access to the WebSocketConnection

The `io.quarkus.websockets.next.WebSocketConnection` object represents the WebSocket connection.
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -463,7 +463,7 @@ private String generateEndpoint(WebSocketEndpointBuildItem endpoint,
MethodDescriptor.ofMethod(WebSocketEndpointBase.class, "beanInstance", Object.class, String.class),
doOnOpen.getThis(), doOnOpen.load(endpoint.bean.getIdentifier()));
// Call the business method
TryBlock tryBlock = onErrorTryBlock(doOnOpen);
TryBlock tryBlock = onErrorTryBlock(doOnOpen, doOnOpen.getThis());
ResultHandle[] args = callback.generateArguments(tryBlock.getThis(), tryBlock, transformedAnnotations, index);
ResultHandle ret = tryBlock.invokeVirtualMethod(MethodDescriptor.of(callback.method), beanInstance, args);
encodeAndReturnResult(tryBlock.getThis(), tryBlock, callback, globalErrorHandlers, endpoint, ret);
Expand All @@ -488,7 +488,7 @@ private String generateEndpoint(WebSocketEndpointBuildItem endpoint,
MethodDescriptor.ofMethod(WebSocketEndpointBase.class, "beanInstance", Object.class, String.class),
doOnClose.getThis(), doOnClose.load(endpoint.bean.getIdentifier()));
// Call the business method
TryBlock tryBlock = onErrorTryBlock(doOnClose);
TryBlock tryBlock = onErrorTryBlock(doOnClose, doOnClose.getThis());
ResultHandle[] args = callback.generateArguments(tryBlock.getThis(), tryBlock, transformedAnnotations, index);
ResultHandle ret = tryBlock.invokeVirtualMethod(MethodDescriptor.of(callback.method), beanInstance, args);
encodeAndReturnResult(tryBlock.getThis(), tryBlock, callback, globalErrorHandlers, endpoint, ret);
Expand Down Expand Up @@ -632,7 +632,7 @@ private void generateOnMessage(ClassCreator endpointCreator, WebSocketEndpointBu
MethodCreator doOnMessage = endpointCreator.getMethodCreator("doOn" + messageType + "Message", Uni.class,
methodParameterType);

TryBlock tryBlock = onErrorTryBlock(doOnMessage);
TryBlock tryBlock = onErrorTryBlock(doOnMessage, doOnMessage.getThis());
// Foo foo = beanInstance("foo");
ResultHandle beanInstance = tryBlock.invokeVirtualMethod(
MethodDescriptor.ofMethod(WebSocketEndpointBase.class, "beanInstance", Object.class, String.class),
Expand Down Expand Up @@ -673,13 +673,13 @@ private TryBlock uniFailureTryBlock(BytecodeCreator method) {
return tryBlock;
}

private TryBlock onErrorTryBlock(BytecodeCreator method) {
private TryBlock onErrorTryBlock(BytecodeCreator method, ResultHandle endpointThis) {
TryBlock tryBlock = method.tryBlock();
CatchBlockCreator catchBlock = tryBlock.addCatch(Throwable.class);
// return doOnError(t);
catchBlock.returnValue(catchBlock.invokeVirtualMethod(
MethodDescriptor.ofMethod(WebSocketEndpointBase.class, "doOnError", Uni.class, Throwable.class),
catchBlock.getThis(), catchBlock.getCaughtException()));
endpointThis, catchBlock.getCaughtException()));
return tryBlock;
}

Expand Down Expand Up @@ -810,23 +810,28 @@ private ResultHandle encodeMessage(ResultHandle endpointThis, BytecodeCreator me
return uniOnFailureDoOnError(endpointThis, method, callback, uniChain, endpoint, globalErrorHandlers);
}
} else if (callback.isReturnTypeMulti()) {
// return multiBinary(multi, broadcast, m -> {
// Buffer buffer = encodeBuffer(m);
// return sendBinary(buffer,broadcast);
//});
// try {
// Buffer buffer = encodeBuffer(m);
// return sendBinary(buffer,broadcast);
// } catch(Throwable t) {
// return doOnError(t);
// }
FunctionCreator fun = method.createFunction(Function.class);
BytecodeCreator funBytecode = fun.getBytecode();
ResultHandle buffer = encodeBuffer(funBytecode, callback.returnType().asParameterizedType().arguments().get(0),
funBytecode.getMethodParam(0), endpointThis, callback);
funBytecode.returnValue(funBytecode.invokeVirtualMethod(
// This checkcast should not be necessary but we need to use the endpoint in the function bytecode
// otherwise gizmo does not access the endpoint reference correcly
ResultHandle endpointBase = funBytecode.checkCast(endpointThis, WebSocketEndpointBase.class);
TryBlock tryBlock = onErrorTryBlock(fun.getBytecode(), endpointBase);
ResultHandle buffer = encodeBuffer(tryBlock, callback.returnType().asParameterizedType().arguments().get(0),
tryBlock.getMethodParam(0), endpointThis, callback);
tryBlock.returnValue(tryBlock.invokeVirtualMethod(
MethodDescriptor.ofMethod(WebSocketEndpointBase.class,
"sendBinary", Uni.class, Buffer.class, boolean.class),
endpointThis, buffer,
funBytecode.load(callback.broadcast())));
tryBlock.load(callback.broadcast())));
return method.invokeVirtualMethod(MethodDescriptor.ofMethod(WebSocketEndpointBase.class,
"multiBinary", Uni.class, Multi.class, boolean.class, Function.class), endpointThis,
"multiBinary", Uni.class, Multi.class, Function.class), endpointThis,
value,
method.load(callback.broadcast()),
fun.getInstance());
} else {
// return sendBinary(buffer,broadcast);
Expand Down Expand Up @@ -865,22 +870,29 @@ private ResultHandle encodeMessage(ResultHandle endpointThis, BytecodeCreator me
}
} else if (callback.isReturnTypeMulti()) {
// return multiText(multi, broadcast, m -> {
// String text = encodeText(m);
// return sendText(buffer,broadcast);
// try {
// String text = encodeText(m);
// return sendText(buffer,broadcast);
// } catch(Throwable t) {
// return doOnError(t);
// }
//});
FunctionCreator fun = method.createFunction(Function.class);
BytecodeCreator funBytecode = fun.getBytecode();
ResultHandle text = encodeText(funBytecode, callback.returnType().asParameterizedType().arguments().get(0),
funBytecode.getMethodParam(0), endpointThis, callback);
funBytecode.returnValue(funBytecode.invokeVirtualMethod(
// This checkcast should not be necessary but we need to use the endpoint in the function bytecode
// otherwise gizmo does not access the endpoint reference correcly
ResultHandle endpointBase = funBytecode.checkCast(endpointThis, WebSocketEndpointBase.class);
TryBlock tryBlock = onErrorTryBlock(fun.getBytecode(), endpointBase);
ResultHandle text = encodeText(tryBlock, callback.returnType().asParameterizedType().arguments().get(0),
tryBlock.getMethodParam(0), endpointThis, callback);
tryBlock.returnValue(tryBlock.invokeVirtualMethod(
MethodDescriptor.ofMethod(WebSocketEndpointBase.class,
"sendText", Uni.class, String.class, boolean.class),
endpointThis, text,
funBytecode.load(callback.broadcast())));
tryBlock.load(callback.broadcast())));
return method.invokeVirtualMethod(MethodDescriptor.ofMethod(WebSocketEndpointBase.class,
"multiText", Uni.class, Multi.class, boolean.class, Function.class), endpointThis,
"multiText", Uni.class, Multi.class, Function.class), endpointThis,
value,
method.load(callback.broadcast()),
fun.getInstance());
} else {
// return sendText(text,broadcast);
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,68 @@
package io.quarkus.websockets.next.test.args;

import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.junit.jupiter.api.Assertions.assertTrue;

import java.net.URI;
import java.util.concurrent.CountDownLatch;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.atomic.AtomicReference;

import jakarta.inject.Inject;

import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.extension.RegisterExtension;

import io.quarkus.test.QuarkusUnitTest;
import io.quarkus.test.common.http.TestHTTPResource;
import io.quarkus.websockets.next.OnClose;
import io.quarkus.websockets.next.OnOpen;
import io.quarkus.websockets.next.PathParam;
import io.quarkus.websockets.next.WebSocket;
import io.quarkus.websockets.next.WebSocketConnection;
import io.quarkus.websockets.next.test.utils.WSClient;
import io.vertx.core.Vertx;
import io.vertx.core.http.WebSocketConnectOptions;

public class OnClosePathParamConnectionArgumentTest {

@RegisterExtension
public static final QuarkusUnitTest test = new QuarkusUnitTest()
.withApplicationRoot(root -> {
root.addClasses(MontyEcho.class, WSClient.class);
});

@Inject
Vertx vertx;

@TestHTTPResource("echo/monty/and/foo")
URI testUri;

@Test
void testArguments() throws InterruptedException {
String header = "fool";
WSClient client = WSClient.create(vertx).connect(new WebSocketConnectOptions().addHeader("X-Test", header), testUri);
client.disconnect();
assertTrue(MontyEcho.CLOSED_LATCH.await(5, TimeUnit.SECONDS));
assertEquals("foo:monty:fool", MontyEcho.CLOSED_MESSAGE.get());
}

@WebSocket(path = "/echo/{grail}/and/{life}")
public static class MontyEcho {

static final CountDownLatch CLOSED_LATCH = new CountDownLatch(1);
static final AtomicReference<String> CLOSED_MESSAGE = new AtomicReference<>();

@OnOpen
void open() {
}

@OnClose
void close(@PathParam String life, @PathParam String grail, WebSocketConnection connection) {
CLOSED_MESSAGE.set(life + ":" + grail + ":" + connection.handshakeRequest().header("X-Test"));
CLOSED_LATCH.countDown();
}

}

}
Original file line number Diff line number Diff line change
@@ -0,0 +1,54 @@
package io.quarkus.websockets.next.test.args;

import static org.junit.jupiter.api.Assertions.assertEquals;

import java.net.URI;

import jakarta.inject.Inject;

import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.extension.RegisterExtension;

import io.quarkus.test.QuarkusUnitTest;
import io.quarkus.test.common.http.TestHTTPResource;
import io.quarkus.websockets.next.OnOpen;
import io.quarkus.websockets.next.PathParam;
import io.quarkus.websockets.next.WebSocket;
import io.quarkus.websockets.next.WebSocketConnection;
import io.quarkus.websockets.next.test.utils.WSClient;
import io.vertx.core.Vertx;
import io.vertx.core.http.WebSocketConnectOptions;

public class OnOpenPathParamConnectionArgumentTest {

@RegisterExtension
public static final QuarkusUnitTest test = new QuarkusUnitTest()
.withApplicationRoot(root -> {
root.addClasses(MontyEcho.class, WSClient.class);
});

@Inject
Vertx vertx;

@TestHTTPResource("echo/monty/and/foo")
URI testUri;

@Test
void testArguments() {
String header = "fool";
WSClient client = WSClient.create(vertx).connect(new WebSocketConnectOptions().addHeader("X-Test", header), testUri);
client.waitForMessages(1);
assertEquals("foo:monty:fool", client.getMessages().get(0).toString());
}

@WebSocket(path = "/echo/{grail}/and/{life}")
public static class MontyEcho {

@OnOpen
String process(@PathParam String life, @PathParam String grail, WebSocketConnection connection) {
return life + ":" + grail + ":" + connection.handshakeRequest().header("X-Test");
}

}

}
Loading
Loading