Skip to content

Commit f959fc0

Browse files
committed
Update /<count>/ endpoints to use a '?count=' query parameter instead
In most RESTful APIs, path parameters are used to represent resources, and query parameters are used to control how these resources are being filtered/sorted/... The old /<count>/ functionality is kept alive to maintain backwards compatibility, but new paths with query parameters are introduced and documented as the default interface so future API methods don't break consistency by using query parameters.
1 parent a094976 commit f959fc0

File tree

3 files changed

+73
-32
lines changed

3 files changed

+73
-32
lines changed

doc/REST-interface.md

Lines changed: 8 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -47,18 +47,24 @@ The HTTP request and response are both handled entirely in-memory.
4747
With the /notxdetails/ option JSON response will only contain the transaction hash instead of the complete transaction details. The option only affects the JSON response.
4848

4949
#### Blockheaders
50-
`GET /rest/headers/<COUNT>/<BLOCK-HASH>.<bin|hex|json>`
50+
`GET /rest/headers/<BLOCK-HASH>.<bin|hex|json>?count=<COUNT=5>`
5151

5252
Given a block hash: returns <COUNT> amount of blockheaders in upward direction.
5353
Returns empty if the block doesn't exist or it isn't in the active chain.
5454

55+
*Deprecated (but not removed) since 24.0:*
56+
`GET /rest/headers/<COUNT>/<BLOCK-HASH>.<bin|hex|json>`
57+
5558
#### Blockfilter Headers
56-
`GET /rest/blockfilterheaders/<FILTERTYPE>/<COUNT>/<BLOCK-HASH>.<bin|hex|json>`
59+
`GET /rest/blockfilterheaders/<FILTERTYPE>/<BLOCK-HASH>.<bin|hex|json>?count=<COUNT=5>`
5760

5861
Given a block hash: returns <COUNT> amount of blockfilter headers in upward
5962
direction for the filter type <FILTERTYPE>.
6063
Returns empty if the block doesn't exist or it isn't in the active chain.
6164

65+
*Deprecated (but not removed) since 24.0:*
66+
`GET /rest/blockfilterheaders/<FILTERTYPE>/<COUNT>/<BLOCK-HASH>.<bin|hex|json>`
67+
6268
#### Blockfilters
6369
`GET /rest/blockfilter/<FILTERTYPE>/<BLOCK-HASH>.<bin|hex|json>`
6470

src/rest.cpp

Lines changed: 35 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -194,15 +194,25 @@ static bool rest_headers(const std::any& context,
194194
std::vector<std::string> path;
195195
boost::split(path, param, boost::is_any_of("/"));
196196

197-
if (path.size() != 2)
198-
return RESTERR(req, HTTP_BAD_REQUEST, "No header count specified. Use /rest/headers/<count>/<hash>.<ext>.");
199-
200-
const auto parsed_count{ToIntegral<size_t>(path[0])};
197+
std::string raw_count;
198+
std::string hashStr;
199+
if (path.size() == 2) {
200+
// deprecated path: /rest/headers/<count>/<hash>
201+
hashStr = path[1];
202+
raw_count = path[0];
203+
} else if (path.size() == 1) {
204+
// new path with query parameter: /rest/headers/<hash>?count=<count>
205+
hashStr = path[0];
206+
raw_count = req->GetQueryParameter("count").value_or("5");
207+
} else {
208+
return RESTERR(req, HTTP_BAD_REQUEST, "Invalid URI format. Expected /rest/headers/<hash>.<ext>?count=<count>");
209+
}
210+
211+
const auto parsed_count{ToIntegral<size_t>(raw_count)};
201212
if (!parsed_count.has_value() || *parsed_count < 1 || *parsed_count > MAX_REST_HEADERS_RESULTS) {
202-
return RESTERR(req, HTTP_BAD_REQUEST, strprintf("Header count is invalid or out of acceptable range (1-%u): %s", MAX_REST_HEADERS_RESULTS, path[0]));
213+
return RESTERR(req, HTTP_BAD_REQUEST, strprintf("Header count is invalid or out of acceptable range (1-%u): %s", MAX_REST_HEADERS_RESULTS, raw_count));
203214
}
204215

205-
std::string hashStr = path[1];
206216
uint256 hash;
207217
if (!ParseHashStr(hashStr, hash))
208218
return RESTERR(req, HTTP_BAD_REQUEST, "Invalid hash: " + hashStr);
@@ -354,13 +364,28 @@ static bool rest_filter_header(const std::any& context, HTTPRequest* req, const
354364

355365
std::vector<std::string> uri_parts;
356366
boost::split(uri_parts, param, boost::is_any_of("/"));
357-
if (uri_parts.size() != 3) {
358-
return RESTERR(req, HTTP_BAD_REQUEST, "Invalid URI format. Expected /rest/blockfilterheaders/<filtertype>/<count>/<blockhash>");
367+
std::string raw_count;
368+
std::string raw_blockhash;
369+
if (uri_parts.size() == 3) {
370+
// deprecated path: /rest/blockfilterheaders/<filtertype>/<count>/<blockhash>
371+
raw_blockhash = uri_parts[2];
372+
raw_count = uri_parts[1];
373+
} else if (uri_parts.size() == 2) {
374+
// new path with query parameter: /rest/blockfilterheaders/<filtertype>/<blockhash>?count=<count>
375+
raw_blockhash = uri_parts[1];
376+
raw_count = req->GetQueryParameter("count").value_or("5");
377+
} else {
378+
return RESTERR(req, HTTP_BAD_REQUEST, "Invalid URI format. Expected /rest/blockfilterheaders/<filtertype>/<blockhash>.<ext>?count=<count>");
379+
}
380+
381+
const auto parsed_count{ToIntegral<size_t>(raw_count)};
382+
if (!parsed_count.has_value() || *parsed_count < 1 || *parsed_count > MAX_REST_HEADERS_RESULTS) {
383+
return RESTERR(req, HTTP_BAD_REQUEST, strprintf("Header count is invalid or out of acceptable range (1-%u): %s", MAX_REST_HEADERS_RESULTS, raw_count));
359384
}
360385

361386
uint256 block_hash;
362-
if (!ParseHashStr(uri_parts[2], block_hash)) {
363-
return RESTERR(req, HTTP_BAD_REQUEST, "Invalid hash: " + uri_parts[2]);
387+
if (!ParseHashStr(raw_blockhash, block_hash)) {
388+
return RESTERR(req, HTTP_BAD_REQUEST, "Invalid hash: " + raw_blockhash);
364389
}
365390

366391
BlockFilterType filtertype;
@@ -373,11 +398,6 @@ static bool rest_filter_header(const std::any& context, HTTPRequest* req, const
373398
return RESTERR(req, HTTP_BAD_REQUEST, "Index is not enabled for filtertype " + uri_parts[0]);
374399
}
375400

376-
const auto parsed_count{ToIntegral<size_t>(uri_parts[1])};
377-
if (!parsed_count.has_value() || *parsed_count < 1 || *parsed_count > MAX_REST_HEADERS_RESULTS) {
378-
return RESTERR(req, HTTP_BAD_REQUEST, strprintf("Header count is invalid or out of acceptable range (1-%u): %s", MAX_REST_HEADERS_RESULTS, uri_parts[1]));
379-
}
380-
381401
std::vector<const CBlockIndex*> headers;
382402
headers.reserve(*parsed_count);
383403
{

test/functional/interface_rest.py

Lines changed: 30 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@
1010
from io import BytesIO
1111
import json
1212
from struct import pack, unpack
13+
import typing
1314
import urllib.parse
1415

1516

@@ -57,14 +58,21 @@ def set_test_params(self):
5758
args.append("[email protected]")
5859
self.supports_cli = False
5960

60-
def test_rest_request(self, uri, http_method='GET', req_type=ReqType.JSON, body='', status=200, ret_type=RetType.JSON):
61+
def test_rest_request(
62+
self,
63+
uri: str,
64+
http_method: str = 'GET',
65+
req_type: ReqType = ReqType.JSON,
66+
body: str = '',
67+
status: int = 200,
68+
ret_type: RetType = RetType.JSON,
69+
query_params: typing.Dict[str, typing.Any] = None,
70+
) -> typing.Union[http.client.HTTPResponse, bytes, str, None]:
6171
rest_uri = '/rest' + uri
62-
if req_type == ReqType.JSON:
63-
rest_uri += '.json'
64-
elif req_type == ReqType.BIN:
65-
rest_uri += '.bin'
66-
elif req_type == ReqType.HEX:
67-
rest_uri += '.hex'
72+
if req_type in ReqType:
73+
rest_uri += f'.{req_type.name.lower()}'
74+
if query_params:
75+
rest_uri += f'?{urllib.parse.urlencode(query_params)}'
6876

6977
conn = http.client.HTTPConnection(self.url.hostname, self.url.port)
7078
self.log.debug(f'{http_method} {rest_uri} {body}')
@@ -83,6 +91,8 @@ def test_rest_request(self, uri, http_method='GET', req_type=ReqType.JSON, body=
8391
elif ret_type == RetType.JSON:
8492
return json.loads(resp.read().decode('utf-8'), parse_float=Decimal)
8593

94+
return None
95+
8696
def run_test(self):
8797
self.url = urllib.parse.urlparse(self.nodes[0].url)
8898
self.wallet = MiniWallet(self.nodes[0])
@@ -213,12 +223,12 @@ def run_test(self):
213223
bb_hash = self.nodes[0].getbestblockhash()
214224

215225
# Check result if block does not exists
216-
assert_equal(self.test_rest_request(f"/headers/1/{UNKNOWN_PARAM}"), [])
226+
assert_equal(self.test_rest_request(f"/headers/{UNKNOWN_PARAM}", query_params={"count": 1}), [])
217227
self.test_rest_request(f"/block/{UNKNOWN_PARAM}", status=404, ret_type=RetType.OBJ)
218228

219229
# Check result if block is not in the active chain
220230
self.nodes[0].invalidateblock(bb_hash)
221-
assert_equal(self.test_rest_request(f'/headers/1/{bb_hash}'), [])
231+
assert_equal(self.test_rest_request(f'/headers/{bb_hash}', query_params={'count': 1}), [])
222232
self.test_rest_request(f'/block/{bb_hash}')
223233
self.nodes[0].reconsiderblock(bb_hash)
224234

@@ -228,7 +238,7 @@ def run_test(self):
228238
response_bytes = response.read()
229239

230240
# Compare with block header
231-
response_header = self.test_rest_request(f"/headers/1/{bb_hash}", req_type=ReqType.BIN, ret_type=RetType.OBJ)
241+
response_header = self.test_rest_request(f"/headers/{bb_hash}", req_type=ReqType.BIN, ret_type=RetType.OBJ, query_params={"count": 1})
232242
assert_equal(int(response_header.getheader('content-length')), BLOCK_HEADER_SIZE)
233243
response_header_bytes = response_header.read()
234244
assert_equal(response_bytes[:BLOCK_HEADER_SIZE], response_header_bytes)
@@ -240,7 +250,7 @@ def run_test(self):
240250
assert_equal(response_bytes.hex().encode(), response_hex_bytes)
241251

242252
# Compare with hex block header
243-
response_header_hex = self.test_rest_request(f"/headers/1/{bb_hash}", req_type=ReqType.HEX, ret_type=RetType.OBJ)
253+
response_header_hex = self.test_rest_request(f"/headers/{bb_hash}", req_type=ReqType.HEX, ret_type=RetType.OBJ, query_params={"count": 1})
244254
assert_greater_than(int(response_header_hex.getheader('content-length')), BLOCK_HEADER_SIZE*2)
245255
response_header_hex_bytes = response_header_hex.read(BLOCK_HEADER_SIZE*2)
246256
assert_equal(response_bytes[:BLOCK_HEADER_SIZE].hex().encode(), response_header_hex_bytes)
@@ -267,7 +277,7 @@ def run_test(self):
267277
self.test_rest_request("/blockhashbyheight/", ret_type=RetType.OBJ, status=400)
268278

269279
# Compare with json block header
270-
json_obj = self.test_rest_request(f"/headers/1/{bb_hash}")
280+
json_obj = self.test_rest_request(f"/headers/{bb_hash}", query_params={"count": 1})
271281
assert_equal(len(json_obj), 1) # ensure that there is one header in the json response
272282
assert_equal(json_obj[0]['hash'], bb_hash) # request/response hash should be the same
273283

@@ -278,9 +288,9 @@ def run_test(self):
278288

279289
# See if we can get 5 headers in one response
280290
self.generate(self.nodes[1], 5)
281-
json_obj = self.test_rest_request(f"/headers/5/{bb_hash}")
291+
json_obj = self.test_rest_request(f"/headers/{bb_hash}", query_params={"count": 5})
282292
assert_equal(len(json_obj), 5) # now we should have 5 header objects
283-
json_obj = self.test_rest_request(f"/blockfilterheaders/basic/5/{bb_hash}")
293+
json_obj = self.test_rest_request(f"/blockfilterheaders/basic/{bb_hash}", query_params={"count": 5})
284294
first_filter_header = json_obj[0]
285295
assert_equal(len(json_obj), 5) # now we should have 5 filter header objects
286296
json_obj = self.test_rest_request(f"/blockfilter/basic/{bb_hash}")
@@ -294,7 +304,7 @@ def run_test(self):
294304
for num in ['5a', '-5', '0', '2001', '99999999999999999999999999999999999']:
295305
assert_equal(
296306
bytes(f'Header count is invalid or out of acceptable range (1-2000): {num}\r\n', 'ascii'),
297-
self.test_rest_request(f"/headers/{num}/{bb_hash}", ret_type=RetType.BYTES, status=400),
307+
self.test_rest_request(f"/headers/{bb_hash}", ret_type=RetType.BYTES, status=400, query_params={"count": num}),
298308
)
299309

300310
self.log.info("Test tx inclusion in the /mempool and /block URIs")
@@ -351,6 +361,11 @@ def run_test(self):
351361
json_obj = self.test_rest_request("/chaininfo")
352362
assert_equal(json_obj['bestblockhash'], bb_hash)
353363

364+
# Test compatibility of deprecated and newer endpoints
365+
self.log.info("Test compatibility of deprecated and newer endpoints")
366+
assert_equal(self.test_rest_request(f"/headers/{bb_hash}", query_params={"count": 1}), self.test_rest_request(f"/headers/1/{bb_hash}"))
367+
assert_equal(self.test_rest_request(f"/blockfilterheaders/basic/{bb_hash}", query_params={"count": 1}), self.test_rest_request(f"/blockfilterheaders/basic/5/{bb_hash}"))
368+
354369

355370
if __name__ == '__main__':
356371
RESTTest().main()

0 commit comments

Comments
 (0)