mirror of https://github.com/hyperledger/besu
Extend error handling of plugin RPC methods (#6759)
Signed-off-by: Fabio Di Fabio <fabio.difabio@consensys.net>pull/6451/head
parent
2bfd510785
commit
86cc6cb19e
@ -0,0 +1,173 @@ |
||||
/* |
||||
* Copyright Hyperledger Besu contributors |
||||
* |
||||
* Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with |
||||
* the License. You may obtain a copy of the License at |
||||
* |
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
* |
||||
* Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on |
||||
* an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the |
||||
* specific language governing permissions and limitations under the License. |
||||
* |
||||
* SPDX-License-Identifier: Apache-2.0 |
||||
*/ |
||||
package org.hyperledger.besu.ethereum.api.jsonrpc; |
||||
|
||||
import static org.assertj.core.api.Assertions.assertThat; |
||||
|
||||
import org.hyperledger.besu.ethereum.api.jsonrpc.internal.exception.InvalidJsonRpcParameters; |
||||
import org.hyperledger.besu.ethereum.api.jsonrpc.internal.methods.PluginJsonRpcMethod; |
||||
import org.hyperledger.besu.ethereum.api.jsonrpc.internal.response.JsonRpcError; |
||||
import org.hyperledger.besu.ethereum.api.jsonrpc.internal.response.RpcErrorType; |
||||
import org.hyperledger.besu.plugin.services.exception.PluginRpcEndpointException; |
||||
import org.hyperledger.besu.plugin.services.rpc.PluginRpcRequest; |
||||
import org.hyperledger.besu.plugin.services.rpc.RpcMethodError; |
||||
|
||||
import io.vertx.core.json.JsonObject; |
||||
import okhttp3.RequestBody; |
||||
import okhttp3.Response; |
||||
import org.junit.jupiter.api.AfterAll; |
||||
import org.junit.jupiter.api.BeforeAll; |
||||
import org.junit.jupiter.api.Test; |
||||
|
||||
public class PluginJsonRpcMethodTest extends JsonRpcHttpServiceTestBase { |
||||
|
||||
@BeforeAll |
||||
public static void setup() throws Exception { |
||||
initServerAndClient(); |
||||
} |
||||
|
||||
/** Tears down the HTTP server. */ |
||||
@AfterAll |
||||
public static void shutdownServer() { |
||||
service.stop().join(); |
||||
} |
||||
|
||||
@Test |
||||
public void happyPath() throws Exception { |
||||
final var request = |
||||
""" |
||||
{"jsonrpc":"2.0","id":1,"method":"plugin_echo","params":["hello"]}"""; |
||||
|
||||
try (var unused = |
||||
addRpcMethod( |
||||
"plugin_echo", |
||||
new PluginJsonRpcMethod("plugin_echo", PluginJsonRpcMethodTest::echoPluginRpcMethod))) { |
||||
final RequestBody body = RequestBody.create(request, JSON); |
||||
|
||||
try (final Response resp = client.newCall(buildPostRequest(body)).execute()) { |
||||
assertThat(resp.code()).isEqualTo(200); |
||||
final JsonObject json = new JsonObject(resp.body().string()); |
||||
testHelper.assertValidJsonRpcResult(json, 1); |
||||
assertThat(json.getString("result")).isEqualTo("hello"); |
||||
} |
||||
} |
||||
} |
||||
|
||||
@Test |
||||
public void invalidJsonShouldReturnParseError() throws Exception { |
||||
final var malformedRequest = |
||||
""" |
||||
{"jsonrpc":"2.0","id":1,"method":"plugin_echo","params":}"""; |
||||
|
||||
try (var unused = |
||||
addRpcMethod( |
||||
"plugin_echo", |
||||
new PluginJsonRpcMethod("plugin_echo", PluginJsonRpcMethodTest::echoPluginRpcMethod))) { |
||||
final RequestBody body = RequestBody.create(malformedRequest, JSON); |
||||
|
||||
try (final Response resp = client.newCall(buildPostRequest(body)).execute()) { |
||||
assertThat(resp.code()).isEqualTo(400); |
||||
final JsonObject json = new JsonObject(resp.body().string()); |
||||
final JsonRpcError expectedError = new JsonRpcError(RpcErrorType.PARSE_ERROR); |
||||
testHelper.assertValidJsonRpcError( |
||||
json, null, expectedError.getCode(), expectedError.getMessage()); |
||||
} |
||||
} |
||||
} |
||||
|
||||
@Test |
||||
public void invalidParamsShouldReturnInvalidParams() throws Exception { |
||||
final var missingRequiredParam = |
||||
""" |
||||
{"jsonrpc":"2.0","id":1,"method":"plugin_echo","params":[]}"""; |
||||
try (var unused = |
||||
addRpcMethod( |
||||
"plugin_echo", |
||||
new PluginJsonRpcMethod("plugin_echo", PluginJsonRpcMethodTest::echoPluginRpcMethod))) { |
||||
final RequestBody body = RequestBody.create(missingRequiredParam, JSON); |
||||
|
||||
try (final Response resp = client.newCall(buildPostRequest(body)).execute()) { |
||||
assertThat(resp.code()).isEqualTo(200); |
||||
final JsonObject json = new JsonObject(resp.body().string()); |
||||
final JsonRpcError expectedError = new JsonRpcError(RpcErrorType.INVALID_PARAMS); |
||||
testHelper.assertValidJsonRpcError( |
||||
json, 1, expectedError.getCode(), expectedError.getMessage()); |
||||
} |
||||
} |
||||
} |
||||
|
||||
@Test |
||||
public void methodErrorShouldReturnErrorResponse() throws Exception { |
||||
final var wrongParamContent = |
||||
""" |
||||
{"jsonrpc":"2.0","id":1,"method":"plugin_echo","params":[" "]}"""; |
||||
try (var unused = |
||||
addRpcMethod( |
||||
"plugin_echo", |
||||
new PluginJsonRpcMethod("plugin_echo", PluginJsonRpcMethodTest::echoPluginRpcMethod))) { |
||||
final RequestBody body = RequestBody.create(wrongParamContent, JSON); |
||||
|
||||
try (final Response resp = client.newCall(buildPostRequest(body)).execute()) { |
||||
assertThat(resp.code()).isEqualTo(200); |
||||
final JsonObject json = new JsonObject(resp.body().string()); |
||||
testHelper.assertValidJsonRpcError(json, 1, -1, "Blank input not allowed"); |
||||
} |
||||
} |
||||
} |
||||
|
||||
@Test |
||||
public void unhandledExceptionShouldReturnInternalErrorResponse() throws Exception { |
||||
final var nullParam = |
||||
""" |
||||
{"jsonrpc":"2.0","id":1,"method":"plugin_echo","params":[null]}"""; |
||||
try (var unused = |
||||
addRpcMethod( |
||||
"plugin_echo", |
||||
new PluginJsonRpcMethod("plugin_echo", PluginJsonRpcMethodTest::echoPluginRpcMethod))) { |
||||
final RequestBody body = RequestBody.create(nullParam, JSON); |
||||
|
||||
try (final Response resp = client.newCall(buildPostRequest(body)).execute()) { |
||||
assertThat(resp.code()).isEqualTo(200); |
||||
final JsonObject json = new JsonObject(resp.body().string()); |
||||
final JsonRpcError expectedError = new JsonRpcError(RpcErrorType.INTERNAL_ERROR); |
||||
testHelper.assertValidJsonRpcError( |
||||
json, 1, expectedError.getCode(), expectedError.getMessage()); |
||||
} |
||||
} |
||||
} |
||||
|
||||
private static Object echoPluginRpcMethod(final PluginRpcRequest request) { |
||||
final var params = request.getParams(); |
||||
if (params.length == 0) { |
||||
throw new InvalidJsonRpcParameters("parameter is mandatory"); |
||||
} |
||||
final var input = params[0]; |
||||
if (input.toString().isBlank()) { |
||||
throw new PluginRpcEndpointException( |
||||
new RpcMethodError() { |
||||
@Override |
||||
public int getCode() { |
||||
return -1; |
||||
} |
||||
|
||||
@Override |
||||
public String getMessage() { |
||||
return "Blank input not allowed"; |
||||
} |
||||
}); |
||||
} |
||||
return input; |
||||
} |
||||
} |
@ -0,0 +1,51 @@ |
||||
/* |
||||
* Copyright Hyperledger Besu Contributors. |
||||
* |
||||
* Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with |
||||
* the License. You may obtain a copy of the License at |
||||
* |
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
* |
||||
* Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on |
||||
* an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the |
||||
* specific language governing permissions and limitations under the License. |
||||
* |
||||
* SPDX-License-Identifier: Apache-2.0 |
||||
*/ |
||||
package org.hyperledger.besu.plugin.services.rpc; |
||||
|
||||
import java.util.Optional; |
||||
|
||||
/** |
||||
* The {@code RpcMethodError} interface defines the structure for RPC error handling within the |
||||
* context of plugins. It provides methods to retrieve error code, message, and an optional data |
||||
* decoder function. |
||||
*/ |
||||
public interface RpcMethodError { |
||||
|
||||
/** |
||||
* Retrieves the error code associated with the RPC error. |
||||
* |
||||
* @return An integer representing the error code. |
||||
*/ |
||||
int getCode(); |
||||
|
||||
/** |
||||
* Retrieves the message associated with the RPC error. |
||||
* |
||||
* @return A {@code String} containing the error message. |
||||
*/ |
||||
String getMessage(); |
||||
|
||||
/** |
||||
* Some errors have additional data associated with them, that is possible to decode to provide a |
||||
* more detailed error response. |
||||
* |
||||
* @param data the additional data to decode |
||||
* @return an optional containing the decoded data if the error has it and the decoding is |
||||
* successful, otherwise empty. |
||||
*/ |
||||
default Optional<String> decodeData(final String data) { |
||||
return Optional.empty(); |
||||
} |
||||
} |
Loading…
Reference in new issue