Skip to content

Commit 75392a9

Browse files
authored
Add support for exception mappers to JSON-RPC (#10663)
* Add support for exception mappers to JSON-RPC. A mapper can optionally return a JsonRpcError. * Send internal error if an exception mapper does not return one. Updates javadocs and tests.
1 parent 0072641 commit 75392a9

File tree

5 files changed

+325
-84
lines changed

5 files changed

+325
-84
lines changed

webclient/jsonrpc/src/main/java/io/helidon/webclient/jsonrpc/JsonRpcClientResponseImpl.java

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -68,7 +68,7 @@ public Optional<JsonRpcResult> result() {
6868
public Optional<JsonRpcError> error() {
6969
try {
7070
JsonObject error = asJsonObject().getJsonObject("error");
71-
return Optional.of(JsonRpcError.create(error));
71+
return Optional.ofNullable(error == null ? null : JsonRpcError.create(error));
7272
} catch (ClassCastException e) {
7373
return Optional.empty();
7474
}
Lines changed: 42 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,42 @@
1+
/*
2+
* Copyright (c) 2025 Oracle and/or its affiliates.
3+
*
4+
* Licensed under the Apache License, Version 2.0 (the "License");
5+
* you may not use this file except in compliance with the License.
6+
* You may obtain a copy of the License at
7+
*
8+
* http://www.apache.org/licenses/LICENSE-2.0
9+
*
10+
* Unless required by applicable law or agreed to in writing, software
11+
* distributed under the License is distributed on an "AS IS" BASIS,
12+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
* See the License for the specific language governing permissions and
14+
* limitations under the License.
15+
*/
16+
package io.helidon.webserver.jsonrpc;
17+
18+
import java.util.Optional;
19+
20+
import io.helidon.jsonrpc.core.JsonRpcError;
21+
import io.helidon.webserver.ServerLifecycle;
22+
23+
/**
24+
* An exception handler that can be registered to map exceptions thrown in method
25+
* handlers to {@code JsonRpcError}s.
26+
*/
27+
@FunctionalInterface
28+
public interface JsonRpcExceptionHandler extends ServerLifecycle {
29+
30+
/**
31+
* Handler for exceptions thrown in JSON-RPC method handlers.
32+
*
33+
* @param req the server request
34+
* @param res the server response
35+
* @param throwable the throwable thrown by the method handler
36+
* @return an optional JSON-RPC error to be returned to the client. If the returned
37+
* optional is empty, then an error with code {@link JsonRpcError#INTERNAL_ERROR}
38+
* is sent instead.
39+
* @throws Exception if an unexpected condition is found
40+
*/
41+
Optional<JsonRpcError> handle(JsonRpcRequest req, JsonRpcResponse res, Throwable throwable) throws Exception;
42+
}

webserver/jsonrpc/src/main/java/io/helidon/webserver/jsonrpc/JsonRpcHandlersBlueprint.java

Lines changed: 9 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -29,7 +29,7 @@
2929
interface JsonRpcHandlersBlueprint {
3030

3131
/**
32-
* Return a map of method names to handles.
32+
* Return a map of method names to handlers.
3333
*
3434
* @return a map of method names to handlers
3535
*/
@@ -42,4 +42,12 @@ interface JsonRpcHandlersBlueprint {
4242
* @return the error handler or {@code null}
4343
*/
4444
Optional<JsonRpcErrorHandler> errorHandler();
45+
46+
/**
47+
* Return a map of throwable to exception handlers.
48+
*
49+
* @return a map of method names to handlers
50+
*/
51+
@Option.Singular(value = "exception", withPrefix = false)
52+
Map<Class<? extends Throwable>, JsonRpcExceptionHandler> exceptionMap();
4553
}

webserver/jsonrpc/src/main/java/io/helidon/webserver/jsonrpc/JsonRpcRouting.java

Lines changed: 118 additions & 82 deletions
Original file line numberDiff line numberDiff line change
@@ -91,121 +91,157 @@ public void routing(HttpRules httpRules) {
9191
JsonRpcErrorHandler errorHandler = handlers.errorHandler().orElse(null);
9292

9393
httpRules.post(pathPattern, (req, res) -> {
94+
// attempt to parse request as JSON
95+
JsonStructure jsonRequest;
9496
try {
95-
// attempt to parse request as JSON
96-
JsonStructure jsonRequest;
97+
jsonRequest = req.content().as(JsonStructure.class);
98+
} catch (JsonParsingException e) {
99+
JsonObject parseError = jsonRpcError(PARSE_ERROR_ERROR, res, null);
100+
res.status(Status.OK_200).send(parseError);
101+
return;
102+
}
103+
104+
// is this a single request?
105+
if (jsonRequest instanceof JsonObject jsonObject) {
106+
// if request fails verification, return JSON-RPC error
107+
JsonRpcError error = verifyJsonRpc(jsonObject, handlersMap);
108+
if (error != null) {
109+
// Use error if returned by error handler
110+
if (errorHandler != null) {
111+
Optional<JsonRpcError> userError = errorHandler.handle(req, jsonObject);
112+
if (userError.isPresent()) {
113+
res.status(Status.OK_200).send(jsonRpcError(userError.get(), res, jsonObject));
114+
} else {
115+
res.status(Status.OK_200).send();
116+
}
117+
} else {
118+
// otherwise return error
119+
JsonObject verifyError = jsonRpcError(error, res, jsonObject);
120+
res.status(Status.OK_200).send(verifyError);
121+
}
122+
return;
123+
}
124+
125+
// prepare and call method handler
126+
AtomicBoolean sendCalled = new AtomicBoolean();
127+
JsonRpcHandler handler = handlersMap.get(jsonObject.getString("method"));
128+
JsonRpcRequest jsonReq = new JsonRpcRequestImpl(req, jsonObject);
129+
JsonValue rpcId = jsonReq.rpcId().orElse(null);
130+
JsonRpcResponse jsonRes = new JsonRpcSingleResponse(rpcId, res, sendCalled);
131+
132+
// invoke single handler
97133
try {
98-
jsonRequest = req.content().as(JsonStructure.class);
99-
} catch (JsonParsingException e) {
100-
JsonObject parseError = jsonRpcError(PARSE_ERROR_ERROR, res, null);
101-
res.status(Status.OK_200).send(parseError);
134+
handler.handle(jsonReq, jsonRes);
135+
// if send() not called, return empty HTTP response
136+
if (!sendCalled.get()) {
137+
res.status(jsonRes.status()).send();
138+
}
139+
} catch (Throwable throwable1) {
140+
try {
141+
// see if there is an exception handler defined
142+
Optional<JsonRpcError> mappedError = handleThrowable(handlers, jsonReq, jsonRes, throwable1);
143+
144+
// use error if returned, otherwise internal error
145+
if (mappedError.isPresent()) {
146+
JsonObject jsonRpcError = jsonRpcError(mappedError.get(), res, null);
147+
res.status(Status.OK_200).send(jsonRpcError);
148+
return;
149+
}
150+
} catch (Throwable throwable2) {
151+
// falls through
152+
}
153+
sendInternalError(res);
154+
}
155+
} else if (jsonRequest instanceof JsonArray jsonArray) {
156+
// we must receive at least one request
157+
int size = jsonArray.size();
158+
if (size == 0) {
159+
sendInvalidRequest(res);
102160
return;
103161
}
104162

105-
// is this a single request?
106-
if (jsonRequest instanceof JsonObject jsonObject) {
107-
// if request fails verification, return JSON-RPC error
163+
// process batch requests
164+
JsonArrayBuilder arrayBuilder = JSON_BUILDER_FACTORY.createArrayBuilder();
165+
for (int i = 0; i < size; i++) {
166+
JsonValue jsonValue = jsonArray.get(i);
167+
168+
// requests must be objects
169+
if (!(jsonValue instanceof JsonObject jsonObject)) {
170+
JsonObject invalidRequest = jsonRpcError(INVALID_REQUEST_ERROR, res, null);
171+
arrayBuilder.add(invalidRequest);
172+
continue; // skip bad request
173+
}
174+
175+
// check if request passes validation before proceeding
108176
JsonRpcError error = verifyJsonRpc(jsonObject, handlersMap);
109177
if (error != null) {
110178
// Use error if returned by error handler
111179
if (errorHandler != null) {
112180
Optional<JsonRpcError> userError = errorHandler.handle(req, jsonObject);
113-
if (userError.isPresent()) {
114-
res.status(Status.OK_200).send(jsonRpcError(userError.get(), res, jsonObject));
115-
} else {
116-
res.status(Status.OK_200).send();
117-
}
181+
userError.ifPresent(e -> arrayBuilder.add(jsonRpcError(e, res, jsonObject)));
118182
} else {
119-
// otherwise return error
120183
JsonObject verifyError = jsonRpcError(error, res, jsonObject);
121-
res.status(Status.OK_200).send(verifyError);
184+
arrayBuilder.add(verifyError);
122185
}
123-
return;
186+
continue;
124187
}
125188

126189
// prepare and call method handler
127-
AtomicBoolean sendCalled = new AtomicBoolean();
128190
JsonRpcHandler handler = handlersMap.get(jsonObject.getString("method"));
129191
JsonRpcRequest jsonReq = new JsonRpcRequestImpl(req, jsonObject);
130192
JsonValue rpcId = jsonReq.rpcId().orElse(null);
131-
JsonRpcResponse jsonRes = new JsonRpcSingleResponse(rpcId, res, sendCalled);
193+
JsonRpcResponse jsonRes = new MyJsonRpcBatchResponse(rpcId, res, arrayBuilder);
132194

133-
// invoke single handler
195+
// invoke handler
134196
try {
135197
handler.handle(jsonReq, jsonRes);
136-
// if send() not called, return empty HTTP response
137-
if (!sendCalled.get()) {
138-
res.status(jsonRes.status()).send();
139-
}
140-
} catch (Exception e) {
141-
sendInternalError(res);
142-
}
143-
} else if (jsonRequest instanceof JsonArray jsonArray) {
144-
// we must receive at least one request
145-
int size = jsonArray.size();
146-
if (size == 0) {
147-
sendInvalidRequest(res);
148-
return;
149-
}
150-
151-
// process batch requests
152-
JsonArrayBuilder arrayBuilder = JSON_BUILDER_FACTORY.createArrayBuilder();
153-
for (int i = 0; i < size; i++) {
154-
JsonValue jsonValue = jsonArray.get(i);
155-
156-
// requests must be objects
157-
if (!(jsonValue instanceof JsonObject jsonObject)) {
158-
JsonObject invalidRequest = jsonRpcError(INVALID_REQUEST_ERROR, res, null);
159-
arrayBuilder.add(invalidRequest);
160-
continue; // skip bad request
161-
}
162-
163-
// check if request passes validation before proceeding
164-
JsonRpcError error = verifyJsonRpc(jsonObject, handlersMap);
165-
if (error != null) {
166-
// Use error if returned by error handler
167-
if (errorHandler != null) {
168-
Optional<JsonRpcError> userError = errorHandler.handle(req, jsonObject);
169-
userError.ifPresent(e -> arrayBuilder.add(jsonRpcError(e, res, jsonObject)));
170-
} else {
171-
JsonObject verifyError = jsonRpcError(error, res, jsonObject);
172-
arrayBuilder.add(verifyError);
173-
}
174-
continue;
175-
}
176-
177-
// prepare and call method handler
178-
JsonRpcHandler handler = handlersMap.get(jsonObject.getString("method"));
179-
JsonRpcRequest jsonReq = new JsonRpcRequestImpl(req, jsonObject);
180-
JsonValue rpcId = jsonReq.rpcId().orElse(null);
181-
JsonRpcResponse jsonRes = new MyJsonRpcBatchResponse(rpcId, res, arrayBuilder);
182-
183-
// invoke handler
198+
} catch (Throwable throwable1) {
184199
try {
185-
handler.handle(jsonReq, jsonRes);
186-
} catch (Exception e) {
187-
sendInternalError(res);
188-
return;
200+
// see if there is an exception handler defined
201+
Optional<JsonRpcError> mappedError = handleThrowable(handlers, jsonReq, jsonRes, throwable1);
202+
203+
// use error if returned, otherwise internal error
204+
if (mappedError.isPresent()) {
205+
JsonObject jsonRpcError = jsonRpcError(mappedError.get(), res, null);
206+
arrayBuilder.add(jsonRpcError);
207+
continue;
208+
}
209+
} catch (Throwable throwable2) {
210+
// falls through
189211
}
212+
JsonObject internalError = jsonRpcError(INTERNAL_ERROR_ERROR, res, null);
213+
arrayBuilder.add(internalError);
190214
}
215+
}
191216

192-
// respond to batch request always with 200
193-
JsonArray result = arrayBuilder.build();
194-
if (result.isEmpty()) {
195-
res.status(Status.OK_200).send();
196-
} else {
197-
res.status(Status.OK_200).send(result);
198-
}
217+
// respond to batch request always with 200
218+
JsonArray result = arrayBuilder.build();
219+
if (result.isEmpty()) {
220+
res.status(Status.OK_200).send();
199221
} else {
200-
sendInvalidRequest(res);
222+
res.status(Status.OK_200).send(result);
201223
}
202-
} catch (Exception e) {
203-
sendInternalError(res);
224+
} else {
225+
sendInvalidRequest(res);
204226
}
205227
});
206228
}
207229
}
208230

231+
private Optional<JsonRpcError> handleThrowable(JsonRpcHandlers handlers,
232+
JsonRpcRequest jsonReq,
233+
JsonRpcResponse jsonRes,
234+
Throwable throwable) throws Throwable {
235+
// returned in registration order
236+
for (Map.Entry<Class<? extends Throwable>, JsonRpcExceptionHandler> entry : handlers.exceptionMap().entrySet()) {
237+
if (entry.getKey().isAssignableFrom(throwable.getClass())) {
238+
JsonRpcExceptionHandler handler = entry.getValue();
239+
return handler.handle(jsonReq, jsonRes, throwable);
240+
}
241+
}
242+
throw throwable; // could not handle exception
243+
}
244+
209245
private JsonRpcError verifyJsonRpc(JsonObject object, Map<String, JsonRpcHandler> handlersMap) {
210246
try {
211247
String version = object.getString("jsonrpc");

0 commit comments

Comments
 (0)