Skip to content

Commit 27cfaee

Browse files
author
MarcoFalke
committed
Merge bitcoin/bitcoin#24098: rest: Use query parameters to control resource loading
54b39cf Add release notes (stickies-v) f959fc0 Update /<count>/ endpoints to use a '?count=' query parameter instead (stickies-v) a094976 Add GetQueryParameter helper function (stickies-v) fff771e Handle query string when parsing data format (stickies-v) c1aad1b scripted-diff: rename RetFormat to RESTResponseFormat (stickies-v) 9f1c547 Refactoring: move declarations to rest.h (stickies-v) Pull request description: In RESTful APIs, [typically](https://rapidapi.com/blog/api-glossary/parameters/query/) path parameters (e.g. `/some/unique/resource/`) are used to represent resources, and query parameters (e.g. `?sort=asc`) are used to control how these resources are being loaded through e.g. sorting, pagination, filtering, ... As first [discussed in #17631](bitcoin/bitcoin#17631 (comment)), the [current REST api](https://github.com/bitcoin/bitcoin/blob/master/doc/REST-interface.md) contains two endpoints `/headers/` and `/blockfilterheaders/` that rather unexpectedly use path parameters to control how many (filter) headers are returned in the response. While this is no critical issue, it is unintuitive and we are still early enough to easily phase this behaviour out and ensure new endpoints (if any) do not have to stick to non-standard behaviour just for internal consistency. In this PR, a new `HTTPRequest::GetQueryParameter` method is introduced to easily parse query parameters, as well as two new `/headers/` and `/blockfilterheaders/` endpoints that use a count query parameter are introduced. The old path parameter-based endpoints are kept without too much overhead, but the documentation now points to the new query parameter-based endpoints as the default interface to encourage standardness. ## Behaviour change ### New endpoints and default values `/headers/` and `/blockfilterheaders/` now have 2 new endpoints that contain query parameters (`?count=<count>`) instead of path parameters (`/<count>/`), as described in REST-interface.md. Since query parameters can easily have default values, I have set this at 5 for both endpoints. **headers** `GET /rest/headers/<BLOCK-HASH>.<bin|hex|json>?count=<COUNT=5>` should now be used instead of `GET /rest/headers/<COUNT>/<BLOCK-HASH>.<bin|hex|json>` **blockfilterheaders** `GET /rest/blockfilterheaders/<FILTERTYPE>/<BLOCK-HASH>.<bin|hex|json>?count=<COUNT=5>` should now be used instead of `GET /rest/blockfilterheaders/<FILTERTYPE>/<COUNT>/<BLOCK-HASH>.<bin|hex|json>` ### Some previously invalid API calls are now valid API calls that contained query strings in the URI could not be parsed prior to this PR. This PR changes behaviour in that previously invalid calls (e.g. `GET /rest/headers/5/somehash.json?someunusedparam=foo`) would now become valid, as the query parameters are properly parsed, and discarded if unused. For example, prior to this PR, adding an irrelevant `someparam` parameter would be illegal: ``` GET /rest/headers/5/0000004c6aad0c89c1c060e8e116dcd849e0554935cd78ff9c6a398abeac6eda.json?someparam=true -> Invalid hash: 0000004c6aad0c89c1c060e8e116dcd849e0554935cd78ff9c6a398abeac6eda.json?someparam=true ``` **This behaviour change affects all rest endpoints, not just the 2 new ones introduced here.** *(Note: I'd be open to implementing additional logic to refuse requests containing unrecognized query parameters to minimize behaviour change, but for the endpoints that we currently have I don't really see the point for that added complexity. E.g. I don't see any scenarios where misspelling a parameter could lead to harmful outcomes)* ## Using the REST API To run the API HTTP server, start a bitcoind instance with the `-rest` flag enabled. To use the `blockfilterheaders` endpoint, you'll also need to set `-blockfilterindex=1`: ``` ./bitcoind -signet -rest -blockfilterindex=1 ``` As soon as bitcoind is fully up and running, you should be able to query the API, for example by using curl on the command line: ```curl "127.0.0.1:38332/rest/chaininfo.json"```. To more easily parse the JSON output, you can also use tools like 'jq' or `json_pp`, e.g.: ``` curl -s "localhost:38332/rest/blockfilterheaders/basic/0000004c6aad0c89c1c060e8e116dcd849e0554935cd78ff9c6a398abeac6eda.json?count=2" | json_pp . ``` ## To do - [x] update `doc/release-notes` ## Feedback This is my first PR (hooray!). Please don't hold back on any feedback/comments/nits/... you may have, big or small, whether they are code, process, language, ... related. I welcome private messages too if there's anything you don't want to clutter the PR with. I'm here to learn and am grateful for everyone's input. ACKs for top commit: stickies-v: I've had to push a tiny doc update to `REST-interface.md` (`git range-diff 219d728 9aac438 54b39cf`) since this was not merged for v23, but since there are no significant changes beyond theStack and jnewbery's ACKs I think this PR is now ready to be considered for merging? @MarcoFalke jnewbery: ACK 54b39cf theStack: re-ACK 54b39cf Tree-SHA512: 3b393ffde34f25605ca12c0b1300799a19684b816a1d03aed38b0f5439df47bfe6a589ffbcd7b83fd2def6c9d00a1bae5e45b1d18df4ae998c617c709990f83f
2 parents 70c5220 + 54b39cf commit 27cfaee

File tree

11 files changed

+333
-95
lines changed

11 files changed

+333
-95
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

doc/release-notes-24098.md

Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,22 @@
1+
Notable changes
2+
===============
3+
4+
Updated REST APIs
5+
-----------------
6+
7+
- The `/headers/` and `/blockfilterheaders/` endpoints have been updated to use
8+
a query parameter instead of path parameter to specify the result count. The
9+
count parameter is now optional, and defaults to 5 for both endpoints. The old
10+
endpoints are still functional, and have no documented behaviour change.
11+
12+
For `/headers`, use
13+
`GET /rest/headers/<BLOCK-HASH>.<bin|hex|json>?count=<COUNT=5>`
14+
instead of
15+
`GET /rest/headers/<COUNT>/<BLOCK-HASH>.<bin|hex|json>` (deprecated)
16+
17+
For `/blockfilterheaders/`, use
18+
`GET /rest/blockfilterheaders/<FILTERTYPE>/<BLOCK-HASH>.<bin|hex|json>?count=<COUNT=5>`
19+
instead of
20+
`GET /rest/blockfilterheaders/<FILTERTYPE>/<COUNT>/<BLOCK-HASH>.<bin|hex|json>` (deprecated)
21+
22+
(#24098)

src/Makefile.am

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -206,6 +206,7 @@ BITCOIN_CORE_H = \
206206
psbt.h \
207207
random.h \
208208
randomenv.h \
209+
rest.h \
209210
reverse_iterator.h \
210211
rpc/blockchain.h \
211212
rpc/client.h \

src/Makefile.test.include

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -94,6 +94,7 @@ BITCOIN_TESTS =\
9494
test/fs_tests.cpp \
9595
test/getarg_tests.cpp \
9696
test/hash_tests.cpp \
97+
test/httpserver_tests.cpp \
9798
test/i2p_tests.cpp \
9899
test/interfaces_tests.cpp \
99100
test/key_io_tests.cpp \
@@ -116,6 +117,7 @@ BITCOIN_TESTS =\
116117
test/prevector_tests.cpp \
117118
test/raii_event_tests.cpp \
118119
test/random_tests.cpp \
120+
test/rest_tests.cpp \
119121
test/reverselock_tests.cpp \
120122
test/rpc_tests.cpp \
121123
test/sanity_tests.cpp \

src/httpserver.cpp

Lines changed: 35 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -23,18 +23,20 @@
2323

2424
#include <deque>
2525
#include <memory>
26+
#include <optional>
2627
#include <stdio.h>
2728
#include <stdlib.h>
2829
#include <string>
2930

3031
#include <sys/types.h>
3132
#include <sys/stat.h>
3233

33-
#include <event2/thread.h>
3434
#include <event2/buffer.h>
3535
#include <event2/bufferevent.h>
36-
#include <event2/util.h>
36+
#include <event2/http.h>
3737
#include <event2/keyvalq_struct.h>
38+
#include <event2/thread.h>
39+
#include <event2/util.h>
3840

3941
#include <support/events.h>
4042

@@ -639,6 +641,37 @@ HTTPRequest::RequestMethod HTTPRequest::GetRequestMethod() const
639641
}
640642
}
641643

644+
std::optional<std::string> HTTPRequest::GetQueryParameter(const std::string& key) const
645+
{
646+
const char* uri{evhttp_request_get_uri(req)};
647+
648+
return GetQueryParameterFromUri(uri, key);
649+
}
650+
651+
std::optional<std::string> GetQueryParameterFromUri(const char* uri, const std::string& key)
652+
{
653+
evhttp_uri* uri_parsed{evhttp_uri_parse(uri)};
654+
const char* query{evhttp_uri_get_query(uri_parsed)};
655+
std::optional<std::string> result;
656+
657+
if (query) {
658+
// Parse the query string into a key-value queue and iterate over it
659+
struct evkeyvalq params_q;
660+
evhttp_parse_query_str(query, &params_q);
661+
662+
for (struct evkeyval* param{params_q.tqh_first}; param != nullptr; param = param->next.tqe_next) {
663+
if (param->key == key) {
664+
result = param->value;
665+
break;
666+
}
667+
}
668+
evhttp_clear_headers(&params_q);
669+
}
670+
evhttp_uri_free(uri_parsed);
671+
672+
return result;
673+
}
674+
642675
void RegisterHTTPHandler(const std::string &prefix, bool exactMatch, const HTTPRequestHandler &handler)
643676
{
644677
LogPrint(BCLog::HTTP, "Registering HTTP handler for %s (exactmatch %d)\n", prefix, exactMatch);

src/httpserver.h

Lines changed: 27 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5,8 +5,9 @@
55
#ifndef BITCOIN_HTTPSERVER_H
66
#define BITCOIN_HTTPSERVER_H
77

8-
#include <string>
98
#include <functional>
9+
#include <optional>
10+
#include <string>
1011

1112
static const int DEFAULT_HTTP_THREADS=4;
1213
static const int DEFAULT_HTTP_WORKQUEUE=16;
@@ -83,6 +84,17 @@ class HTTPRequest
8384
*/
8485
RequestMethod GetRequestMethod() const;
8586

87+
/** Get the query parameter value from request uri for a specified key, or std::nullopt if the
88+
* key is not found.
89+
*
90+
* If the query string contains duplicate keys, the first value is returned. Many web frameworks
91+
* would instead parse this as an array of values, but this is not (yet) implemented as it is
92+
* currently not needed in any of the endpoints.
93+
*
94+
* @param[in] key represents the query parameter of which the value is returned
95+
*/
96+
std::optional<std::string> GetQueryParameter(const std::string& key) const;
97+
8698
/**
8799
* Get the request header specified by hdr, or an empty string.
88100
* Return a pair (isPresent,string).
@@ -115,6 +127,20 @@ class HTTPRequest
115127
void WriteReply(int nStatus, const std::string& strReply = "");
116128
};
117129

130+
/** Get the query parameter value from request uri for a specified key, or std::nullopt if the key
131+
* is not found.
132+
*
133+
* If the query string contains duplicate keys, the first value is returned. Many web frameworks
134+
* would instead parse this as an array of values, but this is not (yet) implemented as it is
135+
* currently not needed in any of the endpoints.
136+
*
137+
* Helper function for HTTPRequest::GetQueryParameter.
138+
*
139+
* @param[in] uri is the entire request uri
140+
* @param[in] key represents the query parameter of which the value is returned
141+
*/
142+
std::optional<std::string> GetQueryParameterFromUri(const char* uri, const std::string& key);
143+
118144
/** Event handler closure.
119145
*/
120146
class HTTPClosure

0 commit comments

Comments
 (0)