diff --git a/chain/jsonrpc/CHANGELOG.md b/chain/jsonrpc/CHANGELOG.md index bc9bc2dbea9..05044dc3fd9 100644 --- a/chain/jsonrpc/CHANGELOG.md +++ b/chain/jsonrpc/CHANGELOG.md @@ -1,5 +1,9 @@ # Changelog +## 2.3.1 + +* Introduced a new status code for a missing block - 422 Unprocessable Content +> Block is considered as missing if rpc returned `UNKNOWN_BLOCK` error while requested block height is less than the latest block height ## 2.3.0 diff --git a/chain/jsonrpc/src/lib.rs b/chain/jsonrpc/src/lib.rs index 3edc46f0bfe..96aac2b9041 100644 --- a/chain/jsonrpc/src/lib.rs +++ b/chain/jsonrpc/src/lib.rs @@ -21,6 +21,7 @@ pub use near_jsonrpc_client as client; pub use near_jsonrpc_primitives as primitives; use near_jsonrpc_primitives::errors::{RpcError, RpcErrorKind}; use near_jsonrpc_primitives::message::{Message, Request}; +use near_jsonrpc_primitives::types::blocks::RpcBlockRequest; use near_jsonrpc_primitives::types::config::{RpcProtocolConfigError, RpcProtocolConfigResponse}; use near_jsonrpc_primitives::types::entity_debug::{EntityDebugHandler, EntityQueryWithParams}; use near_jsonrpc_primitives::types::query::RpcQueryRequest; @@ -1353,21 +1354,51 @@ impl JsonRpcHandler { } } +async fn handle_unknown_block( + request: Message, + handler: web::Data, +) -> actix_web::HttpResponseBuilder { + let Message::Request(request) = request else { + return HttpResponse::Ok(); + }; + + let Some(block_id) = request.params.get("block_id") else { + return HttpResponse::Ok(); + }; + + let Some(block_height) = block_id.as_u64() else { + return HttpResponse::Ok(); + }; + + let Ok(latest_block) = + handler.block(RpcBlockRequest { block_reference: BlockReference::latest() }).await + else { + return HttpResponse::Ok(); + }; + + if block_height < latest_block.block_view.header.height { + return HttpResponse::UnprocessableEntity(); + } + + HttpResponse::Ok() +} + async fn rpc_handler( - message: web::Json, + request: web::Json, handler: web::Data, ) -> HttpResponse { - let message = handler.process(message.0).await; + let message = handler.process(request.0.clone()).await; + let mut response = if let Message::Response(response) = &message { match &response.result { Ok(_) => HttpResponse::Ok(), Err(err) => match &err.error_struct { Some(RpcErrorKind::RequestValidationError(_)) => HttpResponse::BadRequest(), Some(RpcErrorKind::HandlerError(error_struct)) => { - if error_struct["name"] == "TIMEOUT_ERROR" { - HttpResponse::RequestTimeout() - } else { - HttpResponse::Ok() + match error_struct.get("name").and_then(|name| name.as_str()) { + Some("UNKNOWN_BLOCK") => handle_unknown_block(request.0, handler).await, + Some("TIMEOUT_ERROR") => HttpResponse::RequestTimeout(), + _ => HttpResponse::Ok(), } } Some(RpcErrorKind::InternalError(_)) => HttpResponse::InternalServerError(), @@ -1377,6 +1408,7 @@ async fn rpc_handler( } else { HttpResponse::InternalServerError() }; + response.json(message) } diff --git a/nightly/pytest-sanity.txt b/nightly/pytest-sanity.txt index b7fb62e4509..1893c42c216 100644 --- a/nightly/pytest-sanity.txt +++ b/nightly/pytest-sanity.txt @@ -59,6 +59,8 @@ pytest sanity/sync_chunks_from_archival.py pytest sanity/sync_chunks_from_archival.py --features nightly pytest sanity/rpc_tx_forwarding.py pytest sanity/rpc_tx_forwarding.py --features nightly +pytest sanity/rpc_missing_block.py +pytest sanity/rpc_missing_block.py --features nightly pytest --timeout=240 sanity/one_val.py pytest --timeout=240 sanity/one_val.py nightly --features nightly pytest --timeout=240 sanity/lightclnt.py @@ -205,4 +207,4 @@ pytest --timeout=120 sanity/kickout_offline_validators.py --features nightly # Epoch sync pytest --timeout=240 sanity/epoch_sync.py -pytest --timeout=240 sanity/epoch_sync.py --features nightly \ No newline at end of file +pytest --timeout=240 sanity/epoch_sync.py --features nightly diff --git a/pytest/tests/sanity/rpc_missing_block.py b/pytest/tests/sanity/rpc_missing_block.py new file mode 100644 index 00000000000..366717f2595 --- /dev/null +++ b/pytest/tests/sanity/rpc_missing_block.py @@ -0,0 +1,103 @@ +import sys, time +import pathlib + +sys.path.append(str(pathlib.Path(__file__).resolve().parents[2] / "lib")) + +from cluster import start_cluster +from configured_logger import logger +import utils +from geventhttpclient import useragent + +nodes = start_cluster( + num_nodes=4, + num_observers=1, + num_shards=4, + config=None, + extra_state_dumper=True, + genesis_config_changes=[ + ["min_gas_price", 0], + ["max_inflation_rate", [0, 1]], + ["epoch_length", 10], + ["block_producer_kickout_threshold", 70], + ], + client_config_changes={ + 0: { + "consensus": { + "state_sync_timeout": { + "secs": 2, + "nanos": 0 + } + } + }, + 1: { + "consensus": { + "state_sync_timeout": { + "secs": 2, + "nanos": 0 + } + } + }, + 2: { + "consensus": { + "state_sync_timeout": { + "secs": 2, + "nanos": 0 + } + } + }, + 3: { + "consensus": { + "state_sync_timeout": { + "secs": 2, + "nanos": 0 + } + } + }, + 4: { + "consensus": { + "state_sync_timeout": { + "secs": 2, + "nanos": 0 + } + }, + "tracked_shards": [0, 1, 2, 3], + }, + }, +) + +nodes[1].kill() + + +def check_bad_block(node, height): + try: + node.get_block_by_height(height) + assert False, f"Expected an exception for block height {height} but none was raised" + except useragent.BadStatusCode as e: + assert "code=422" in str( + e), f"Expected status code 422 in exception, got: {e}" + except Exception as e: + assert False, f"Unexpected exception type raised: {type(e)}. Exception: {e}" + + +last_height = -1 + +for height, hash in utils.poll_blocks(nodes[0]): + if height >= 20: + break + + response = nodes[0].get_block_by_height(height) + assert not "error" in response + logger.info(f"good RPC response for: {height}") + + if last_height != -1: + for bad_height in range(last_height + 1, height): + response = check_bad_block(nodes[0], bad_height) + logger.info(f"422 response for: {bad_height}") + + last_height = height + +response = nodes[0].get_block_by_height(last_height + 9999) +assert "error" in response, f"Expected an error for block height 9999, got: {response}" +assert ( + response["error"]["cause"]["name"] == "UNKNOWN_BLOCK" +), f"Expected UNKNOWN_BLOCK error, got: {response['error']['cause']['name']}"