mirror of
https://github.com/bitcoin/bitcoin.git
synced 2026-09-12 05:32:22 +02:00
Merge bitcoin/bitcoin#34794: rest: add Cache-Control headers to REST responses
75f5851927doc: add release note for REST cache-control headers (w0xlt)bbe21ac29fdoc: document REST cache-control defaults (w0xlt)862a179556http: add no-store to dispatcher-generated error responses (w0xlt)acf45c44c0rest: add Cache-Control headers to REST responses (w0xlt) Pull request description: This PR adds explicit Cache-Control headers to REST responses. The policy is: - Immutable data gets: `Cache-Control: public, immutable, max-age=86400` - Mutable, node-local, and error responses get: `Cache-Control: no-store` Important details: - `/block` and `/block/notxdetails` bin/hex, `/blockpart`, `/blockfilter`, `/spenttxouts`, and `/deploymentinfo/<blockhash>.json` are treated as immutable. - `/block` and `/block/notxdetails` JSON, all `/tx` formats, `/headers`, `/blockfilterheaders`, `/blockhashbyheight`, `/chaininfo`, `/mempool`, `/getutxos`, and `/deploymentinfo.json` are no-store. - REST errors and HTTP dispatcher-generated errors are no-store. - Unmatched `/rest` 404s also return no-store, including paths like `/rest/tx`, `/rest/does-not-exist`, and `/rest?x=1`. Tests were added in `interface_rest.py` to cover successful responses, behavior across a newly mined block, REST errors, and unmatched REST 404s. Docs were added to `REST-interface.md`, including guidance for overriding the defaults in a reverse proxy or CDN. Closes #33809 ACKs for top commit: stickies-v: re-ACK75f5851927pinheadmz: ACK75f5851927sedited: ACK75f5851927Tree-SHA512: 292ccd06ddfc9272c17fa720ce1ea8bb05462337af6460488f70003d3daf31fcf262e68c264522a911bba65ae2b25fc88a1fd422e5664583daf64070231cb062
This commit is contained in:
@@ -12,6 +12,25 @@ REST Interface consistency guarantees
|
||||
The [same guarantees as for the RPC Interface](/doc/JSON-RPC-interface.md#rpc-consistency-guarantees)
|
||||
apply.
|
||||
|
||||
Default HTTP caching
|
||||
--------------------
|
||||
|
||||
REST responses include `Cache-Control` headers by default:
|
||||
|
||||
* `public, immutable, max-age=86400` for `/block` and `/block/notxdetails`
|
||||
binary and hex responses, `/blockpart`, `/blockfilter` and `/spenttxouts` in
|
||||
all formats, and `/deploymentinfo/<BLOCKHASH>.json`. The TTL is deliberately
|
||||
short so caches do not hold older response shapes across software upgrades.
|
||||
* `no-store` for `/block` and `/block/notxdetails` JSON, `/tx`, `/headers`,
|
||||
`/blockfilterheaders`, `/blockhashbyheight`, `/chaininfo`, `/mempool`,
|
||||
`/getutxos`, `/deploymentinfo.json`, and all error responses. These responses
|
||||
can change with active chain or node state and do not currently provide cache
|
||||
validators such as `ETag` or `Last-Modified`.
|
||||
|
||||
If you front `bitcoind` with a reverse proxy or CDN such as Caddy or nginx with
|
||||
the headers-more module, you can override these defaults there. Keep overrides
|
||||
scoped to responses you know are safe to cache more aggressively.
|
||||
|
||||
Limitations
|
||||
-----------
|
||||
|
||||
|
||||
8
doc/release-notes-34794.md
Normal file
8
doc/release-notes-34794.md
Normal file
@@ -0,0 +1,8 @@
|
||||
REST API
|
||||
--------
|
||||
|
||||
- REST responses now include `Cache-Control` headers to guide intermediary
|
||||
caches. Immutable responses such as block binary and hex data, block parts,
|
||||
block filters, spent transaction outputs, and block-specific deployment info
|
||||
are marked cacheable for one day. Responses that can change with active chain
|
||||
or node state, as well as errors, are marked `no-store`. (#34794)
|
||||
@@ -129,13 +129,19 @@ std::string_view RequestMethodString(HTTPRequestMethod m)
|
||||
assert(false);
|
||||
}
|
||||
|
||||
static void WriteNoStoreErrorReply(HTTPRequest& req, HTTPStatusCode status, std::string_view reply = {})
|
||||
{
|
||||
req.WriteHeader("Cache-Control", "no-store");
|
||||
req.WriteReply(status, reply);
|
||||
}
|
||||
|
||||
static void MaybeDispatchRequestToWorker(std::shared_ptr<HTTPRequest> hreq)
|
||||
{
|
||||
// Early reject unknown HTTP methods
|
||||
if (hreq->GetRequestMethod() == HTTPRequestMethod::UNKNOWN) {
|
||||
LogDebug(BCLog::HTTP, "HTTP request from %s rejected: Unknown HTTP request method\n",
|
||||
hreq->GetPeer().ToStringAddrPort());
|
||||
hreq->WriteReply(HTTP_BAD_METHOD);
|
||||
WriteNoStoreErrorReply(*hreq, HTTP_BAD_METHOD);
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -161,7 +167,7 @@ static void MaybeDispatchRequestToWorker(std::shared_ptr<HTTPRequest> hreq)
|
||||
if (i != iend) {
|
||||
if (static_cast<int>(g_threadpool_http.WorkQueueSize()) >= g_max_queue_depth) {
|
||||
LogWarning("Request rejected because http work queue depth exceeded, it can be increased with the -rpcworkqueue= setting");
|
||||
hreq->WriteReply(HTTP_SERVICE_UNAVAILABLE, "Work queue depth exceeded");
|
||||
WriteNoStoreErrorReply(*hreq, HTTP_SERVICE_UNAVAILABLE, "Work queue depth exceeded");
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -180,25 +186,25 @@ static void MaybeDispatchRequestToWorker(std::shared_ptr<HTTPRequest> hreq)
|
||||
// Reply so the client doesn't hang waiting for the response.
|
||||
req->WriteHeader("Connection", "close");
|
||||
// TODO: Implement specific error formatting for the REST and JSON-RPC servers responses.
|
||||
req->WriteReply(HTTP_INTERNAL_SERVER_ERROR, err_msg);
|
||||
WriteNoStoreErrorReply(*req, HTTP_INTERNAL_SERVER_ERROR, err_msg);
|
||||
};
|
||||
|
||||
if (auto res = g_threadpool_http.Submit(std::move(item)); !res.has_value()) {
|
||||
Assume(hreq.use_count() == 1); // ensure request will be deleted
|
||||
// Both SubmitError::Inactive and SubmitError::Interrupted mean shutdown
|
||||
LogWarning("HTTP request rejected during server shutdown: '%s'", SubmitErrorString(res.error()));
|
||||
hreq->WriteReply(HTTP_SERVICE_UNAVAILABLE, "Request rejected during server shutdown");
|
||||
WriteNoStoreErrorReply(*hreq, HTTP_SERVICE_UNAVAILABLE, "Request rejected during server shutdown");
|
||||
return;
|
||||
}
|
||||
} else {
|
||||
hreq->WriteReply(HTTP_NOT_FOUND);
|
||||
WriteNoStoreErrorReply(*hreq, HTTP_NOT_FOUND);
|
||||
}
|
||||
}
|
||||
|
||||
static void RejectRequest(std::unique_ptr<http_bitcoin::HTTPRequest> hreq)
|
||||
{
|
||||
LogDebug(BCLog::HTTP, "Rejecting request while shutting down");
|
||||
hreq->WriteReply(HTTP_SERVICE_UNAVAILABLE);
|
||||
WriteNoStoreErrorReply(*hreq, HTTP_SERVICE_UNAVAILABLE);
|
||||
}
|
||||
|
||||
static std::vector<std::pair<std::string, uint16_t>> GetBindAddresses()
|
||||
@@ -1005,7 +1011,7 @@ void HTTPServer::MaybeDispatchRequestsFromClient(const std::shared_ptr<HTTPRemot
|
||||
client->m_id,
|
||||
e.what());
|
||||
|
||||
req->WriteReply(HTTP_CONTENT_TOO_LARGE);
|
||||
WriteNoStoreErrorReply(*req, HTTP_CONTENT_TOO_LARGE);
|
||||
client->m_disconnect = true;
|
||||
return;
|
||||
} catch (const std::runtime_error& e) {
|
||||
@@ -1017,7 +1023,7 @@ void HTTPServer::MaybeDispatchRequestsFromClient(const std::shared_ptr<HTTPRemot
|
||||
e.what());
|
||||
|
||||
// We failed to read a complete request from the buffer
|
||||
req->WriteReply(HTTP_BAD_REQUEST);
|
||||
WriteNoStoreErrorReply(*req, HTTP_BAD_REQUEST);
|
||||
client->m_disconnect = true;
|
||||
return;
|
||||
}
|
||||
|
||||
41
src/rest.cpp
41
src/rest.cpp
@@ -45,6 +45,12 @@ using util::SplitString;
|
||||
static const size_t MAX_GETUTXOS_OUTPOINTS = 15; //allow a max of 15 outpoints to be queried at once
|
||||
static constexpr unsigned int MAX_REST_HEADERS_RESULTS = 2000;
|
||||
|
||||
// Cache-Control values for REST responses.
|
||||
/** Response bytes never change. One-day TTL limits staleness across software upgrades. */
|
||||
static constexpr const char* REST_CACHE_IMMUTABLE = "public, immutable, max-age=86400";
|
||||
/** Mutable, node-local, or error response; must not be cached. */
|
||||
static constexpr const char* REST_CACHE_NO_STORE = "no-store";
|
||||
|
||||
static const struct {
|
||||
RESTResponseFormat rf;
|
||||
const char* name;
|
||||
@@ -71,6 +77,7 @@ struct CCoin {
|
||||
|
||||
static bool RESTERR(HTTPRequest* req, enum HTTPStatusCode status, std::string message)
|
||||
{
|
||||
req->WriteHeader("Cache-Control", REST_CACHE_NO_STORE);
|
||||
req->WriteHeader("Content-Type", "text/plain");
|
||||
req->WriteReply(status, message + "\r\n");
|
||||
return false;
|
||||
@@ -242,6 +249,8 @@ static bool rest_headers(const std::any& context,
|
||||
ssHeader << pindex->GetBlockHeader();
|
||||
}
|
||||
|
||||
// Do not cache because chain extensions and reorgs can affect the response.
|
||||
req->WriteHeader("Cache-Control", REST_CACHE_NO_STORE);
|
||||
req->WriteHeader("Content-Type", "application/octet-stream");
|
||||
req->WriteReply(HTTP_OK, ssHeader);
|
||||
return true;
|
||||
@@ -254,6 +263,7 @@ static bool rest_headers(const std::any& context,
|
||||
}
|
||||
|
||||
std::string strHex = HexStr(ssHeader) + "\n";
|
||||
req->WriteHeader("Cache-Control", REST_CACHE_NO_STORE);
|
||||
req->WriteHeader("Content-Type", "text/plain");
|
||||
req->WriteReply(HTTP_OK, strHex);
|
||||
return true;
|
||||
@@ -264,6 +274,7 @@ static bool rest_headers(const std::any& context,
|
||||
jsonHeaders.push_back(blockheaderToJSON(*tip, *pindex, chainman.GetConsensus().powLimit));
|
||||
}
|
||||
std::string strJSON = jsonHeaders.write() + "\n";
|
||||
req->WriteHeader("Cache-Control", REST_CACHE_NO_STORE);
|
||||
req->WriteHeader("Content-Type", "application/json");
|
||||
req->WriteReply(HTTP_OK, strJSON);
|
||||
return true;
|
||||
@@ -352,6 +363,7 @@ static bool rest_spent_txouts(const std::any& context, HTTPRequest* req, const s
|
||||
case RESTResponseFormat::BINARY: {
|
||||
DataStream ssSpentResponse{};
|
||||
SerializeBlockUndo(ssSpentResponse, block_undo);
|
||||
req->WriteHeader("Cache-Control", REST_CACHE_IMMUTABLE);
|
||||
req->WriteHeader("Content-Type", "application/octet-stream");
|
||||
req->WriteReply(HTTP_OK, ssSpentResponse);
|
||||
return true;
|
||||
@@ -361,6 +373,7 @@ static bool rest_spent_txouts(const std::any& context, HTTPRequest* req, const s
|
||||
DataStream ssSpentResponse{};
|
||||
SerializeBlockUndo(ssSpentResponse, block_undo);
|
||||
const std::string strHex{HexStr(ssSpentResponse) + "\n"};
|
||||
req->WriteHeader("Cache-Control", REST_CACHE_IMMUTABLE);
|
||||
req->WriteHeader("Content-Type", "text/plain");
|
||||
req->WriteReply(HTTP_OK, strHex);
|
||||
return true;
|
||||
@@ -370,6 +383,7 @@ static bool rest_spent_txouts(const std::any& context, HTTPRequest* req, const s
|
||||
UniValue result(UniValue::VARR);
|
||||
BlockUndoToJSON(block_undo, result);
|
||||
std::string strJSON = result.write() + "\n";
|
||||
req->WriteHeader("Cache-Control", REST_CACHE_IMMUTABLE);
|
||||
req->WriteHeader("Content-Type", "application/json");
|
||||
req->WriteReply(HTTP_OK, strJSON);
|
||||
return true;
|
||||
@@ -438,6 +452,7 @@ static bool rest_block(const std::any& context,
|
||||
|
||||
switch (rf) {
|
||||
case RESTResponseFormat::BINARY: {
|
||||
req->WriteHeader("Cache-Control", REST_CACHE_IMMUTABLE);
|
||||
req->WriteHeader("Content-Type", "application/octet-stream");
|
||||
req->WriteReply(HTTP_OK, *block_data);
|
||||
return true;
|
||||
@@ -445,6 +460,7 @@ static bool rest_block(const std::any& context,
|
||||
|
||||
case RESTResponseFormat::HEX: {
|
||||
const std::string strHex{HexStr(*block_data) + "\n"};
|
||||
req->WriteHeader("Cache-Control", REST_CACHE_IMMUTABLE);
|
||||
req->WriteHeader("Content-Type", "text/plain");
|
||||
req->WriteReply(HTTP_OK, strHex);
|
||||
return true;
|
||||
@@ -456,6 +472,7 @@ static bool rest_block(const std::any& context,
|
||||
SpanReader{*block_data} >> TX_WITH_WITNESS(block);
|
||||
UniValue objBlock = blockToJSON(chainman.m_blockman, block, *tip, *pblockindex, *tx_verbosity, chainman.GetConsensus().powLimit);
|
||||
std::string strJSON = objBlock.write() + "\n";
|
||||
req->WriteHeader("Cache-Control", REST_CACHE_NO_STORE);
|
||||
req->WriteHeader("Content-Type", "application/json");
|
||||
req->WriteReply(HTTP_OK, strJSON);
|
||||
return true;
|
||||
@@ -588,6 +605,8 @@ static bool rest_filter_header(const std::any& context, HTTPRequest* req, const
|
||||
ssHeader << header;
|
||||
}
|
||||
|
||||
// Do not cache because chain extensions and reorgs can affect the response.
|
||||
req->WriteHeader("Cache-Control", REST_CACHE_NO_STORE);
|
||||
req->WriteHeader("Content-Type", "application/octet-stream");
|
||||
req->WriteReply(HTTP_OK, ssHeader);
|
||||
return true;
|
||||
@@ -599,6 +618,7 @@ static bool rest_filter_header(const std::any& context, HTTPRequest* req, const
|
||||
}
|
||||
|
||||
std::string strHex = HexStr(ssHeader) + "\n";
|
||||
req->WriteHeader("Cache-Control", REST_CACHE_NO_STORE);
|
||||
req->WriteHeader("Content-Type", "text/plain");
|
||||
req->WriteReply(HTTP_OK, strHex);
|
||||
return true;
|
||||
@@ -610,6 +630,7 @@ static bool rest_filter_header(const std::any& context, HTTPRequest* req, const
|
||||
}
|
||||
|
||||
std::string strJSON = jsonHeaders.write() + "\n";
|
||||
req->WriteHeader("Cache-Control", REST_CACHE_NO_STORE);
|
||||
req->WriteHeader("Content-Type", "application/json");
|
||||
req->WriteReply(HTTP_OK, strJSON);
|
||||
return true;
|
||||
@@ -684,6 +705,7 @@ static bool rest_block_filter(const std::any& context, HTTPRequest* req, const s
|
||||
DataStream ssResp{};
|
||||
ssResp << filter;
|
||||
|
||||
req->WriteHeader("Cache-Control", REST_CACHE_IMMUTABLE);
|
||||
req->WriteHeader("Content-Type", "application/octet-stream");
|
||||
req->WriteReply(HTTP_OK, ssResp);
|
||||
return true;
|
||||
@@ -693,6 +715,7 @@ static bool rest_block_filter(const std::any& context, HTTPRequest* req, const s
|
||||
ssResp << filter;
|
||||
|
||||
std::string strHex = HexStr(ssResp) + "\n";
|
||||
req->WriteHeader("Cache-Control", REST_CACHE_IMMUTABLE);
|
||||
req->WriteHeader("Content-Type", "text/plain");
|
||||
req->WriteReply(HTTP_OK, strHex);
|
||||
return true;
|
||||
@@ -701,6 +724,7 @@ static bool rest_block_filter(const std::any& context, HTTPRequest* req, const s
|
||||
UniValue ret(UniValue::VOBJ);
|
||||
ret.pushKV("filter", HexStr(filter.GetEncodedFilter()));
|
||||
std::string strJSON = ret.write() + "\n";
|
||||
req->WriteHeader("Cache-Control", REST_CACHE_IMMUTABLE);
|
||||
req->WriteHeader("Content-Type", "application/json");
|
||||
req->WriteReply(HTTP_OK, strJSON);
|
||||
return true;
|
||||
@@ -728,6 +752,7 @@ static bool rest_chaininfo(const std::any& context, HTTPRequest* req, const std:
|
||||
jsonRequest.params = UniValue(UniValue::VARR);
|
||||
UniValue chainInfoObject = getblockchaininfo().HandleRequest(jsonRequest);
|
||||
std::string strJSON = chainInfoObject.write() + "\n";
|
||||
req->WriteHeader("Cache-Control", REST_CACHE_NO_STORE);
|
||||
req->WriteHeader("Content-Type", "application/json");
|
||||
req->WriteReply(HTTP_OK, strJSON);
|
||||
return true;
|
||||
@@ -747,6 +772,7 @@ static bool rest_deploymentinfo(const std::any& context, HTTPRequest* req, const
|
||||
|
||||
std::string hash_str;
|
||||
const RESTResponseFormat rf = ParseDataFormat(hash_str, str_uri_part);
|
||||
const bool current_tip{hash_str.empty()};
|
||||
|
||||
switch (rf) {
|
||||
case RESTResponseFormat::JSON: {
|
||||
@@ -754,7 +780,7 @@ static bool rest_deploymentinfo(const std::any& context, HTTPRequest* req, const
|
||||
jsonRequest.context = context;
|
||||
jsonRequest.params = UniValue(UniValue::VARR);
|
||||
|
||||
if (!hash_str.empty()) {
|
||||
if (!current_tip) {
|
||||
auto hash{uint256::FromHex(hash_str)};
|
||||
if (!hash) {
|
||||
return RESTERR(req, HTTP_BAD_REQUEST, "Invalid hash: " + hash_str);
|
||||
@@ -769,6 +795,7 @@ static bool rest_deploymentinfo(const std::any& context, HTTPRequest* req, const
|
||||
jsonRequest.params.push_back(hash_str);
|
||||
}
|
||||
|
||||
req->WriteHeader("Cache-Control", current_tip ? REST_CACHE_NO_STORE : REST_CACHE_IMMUTABLE);
|
||||
req->WriteHeader("Content-Type", "application/json");
|
||||
req->WriteReply(HTTP_OK, getdeploymentinfo().HandleRequest(jsonRequest).write() + "\n");
|
||||
return true;
|
||||
@@ -826,6 +853,7 @@ static bool rest_mempool(const std::any& context, HTTPRequest* req, const std::s
|
||||
str_json = MempoolInfoToJSON(*mempool).write() + "\n";
|
||||
}
|
||||
|
||||
req->WriteHeader("Cache-Control", REST_CACHE_NO_STORE);
|
||||
req->WriteHeader("Content-Type", "application/json");
|
||||
req->WriteReply(HTTP_OK, str_json);
|
||||
return true;
|
||||
@@ -859,12 +887,12 @@ static bool rest_tx(const std::any& context, HTTPRequest* req, const std::string
|
||||
if (!tx) {
|
||||
return RESTERR(req, HTTP_NOT_FOUND, hashStr + " not found");
|
||||
}
|
||||
|
||||
switch (rf) {
|
||||
case RESTResponseFormat::BINARY: {
|
||||
DataStream ssTx;
|
||||
ssTx << TX_WITH_WITNESS(tx);
|
||||
|
||||
req->WriteHeader("Cache-Control", REST_CACHE_NO_STORE);
|
||||
req->WriteHeader("Content-Type", "application/octet-stream");
|
||||
req->WriteReply(HTTP_OK, ssTx);
|
||||
return true;
|
||||
@@ -875,6 +903,7 @@ static bool rest_tx(const std::any& context, HTTPRequest* req, const std::string
|
||||
ssTx << TX_WITH_WITNESS(tx);
|
||||
|
||||
std::string strHex = HexStr(ssTx) + "\n";
|
||||
req->WriteHeader("Cache-Control", REST_CACHE_NO_STORE);
|
||||
req->WriteHeader("Content-Type", "text/plain");
|
||||
req->WriteReply(HTTP_OK, strHex);
|
||||
return true;
|
||||
@@ -884,6 +913,7 @@ static bool rest_tx(const std::any& context, HTTPRequest* req, const std::string
|
||||
UniValue objTx(UniValue::VOBJ);
|
||||
TxToUniv(*tx, /*block_hash=*/hashBlock, /*entry=*/ objTx);
|
||||
std::string strJSON = objTx.write() + "\n";
|
||||
req->WriteHeader("Cache-Control", REST_CACHE_NO_STORE);
|
||||
req->WriteHeader("Content-Type", "application/json");
|
||||
req->WriteReply(HTTP_OK, strJSON);
|
||||
return true;
|
||||
@@ -1039,6 +1069,7 @@ static bool rest_getutxos(const std::any& context, HTTPRequest* req, const std::
|
||||
DataStream ssGetUTXOResponse{};
|
||||
ssGetUTXOResponse << active_height << active_hash << bitmap << outs;
|
||||
|
||||
req->WriteHeader("Cache-Control", REST_CACHE_NO_STORE);
|
||||
req->WriteHeader("Content-Type", "application/octet-stream");
|
||||
req->WriteReply(HTTP_OK, ssGetUTXOResponse);
|
||||
return true;
|
||||
@@ -1049,6 +1080,7 @@ static bool rest_getutxos(const std::any& context, HTTPRequest* req, const std::
|
||||
ssGetUTXOResponse << active_height << active_hash << bitmap << outs;
|
||||
std::string strHex = HexStr(ssGetUTXOResponse) + "\n";
|
||||
|
||||
req->WriteHeader("Cache-Control", REST_CACHE_NO_STORE);
|
||||
req->WriteHeader("Content-Type", "text/plain");
|
||||
req->WriteReply(HTTP_OK, strHex);
|
||||
return true;
|
||||
@@ -1079,6 +1111,7 @@ static bool rest_getutxos(const std::any& context, HTTPRequest* req, const std::
|
||||
|
||||
// return json string
|
||||
std::string strJSON = objGetUTXOResponse.write() + "\n";
|
||||
req->WriteHeader("Cache-Control", REST_CACHE_NO_STORE);
|
||||
req->WriteHeader("Content-Type", "application/json");
|
||||
req->WriteReply(HTTP_OK, strJSON);
|
||||
return true;
|
||||
@@ -1117,16 +1150,20 @@ static bool rest_blockhash_by_height(const std::any& context, HTTPRequest* req,
|
||||
case RESTResponseFormat::BINARY: {
|
||||
DataStream ss_blockhash{};
|
||||
ss_blockhash << pblockindex->GetBlockHash();
|
||||
// Do not cache because reorgs can change the response.
|
||||
req->WriteHeader("Cache-Control", REST_CACHE_NO_STORE);
|
||||
req->WriteHeader("Content-Type", "application/octet-stream");
|
||||
req->WriteReply(HTTP_OK, ss_blockhash);
|
||||
return true;
|
||||
}
|
||||
case RESTResponseFormat::HEX: {
|
||||
req->WriteHeader("Cache-Control", REST_CACHE_NO_STORE);
|
||||
req->WriteHeader("Content-Type", "text/plain");
|
||||
req->WriteReply(HTTP_OK, pblockindex->GetBlockHash().GetHex() + "\n");
|
||||
return true;
|
||||
}
|
||||
case RESTResponseFormat::JSON: {
|
||||
req->WriteHeader("Cache-Control", REST_CACHE_NO_STORE);
|
||||
req->WriteHeader("Content-Type", "application/json");
|
||||
UniValue resp = UniValue(UniValue::VOBJ);
|
||||
resp.pushKV("blockhash", pblockindex->GetBlockHash().GetHex());
|
||||
|
||||
@@ -23,6 +23,8 @@ from test_framework.util import (
|
||||
assert_equal,
|
||||
assert_greater_than,
|
||||
assert_greater_than_or_equal,
|
||||
assert_not_equal,
|
||||
sync_txindex,
|
||||
)
|
||||
from test_framework.wallet import (
|
||||
MiniWallet,
|
||||
@@ -52,7 +54,7 @@ def filter_output_indices_by_value(vouts, value):
|
||||
class RESTTest (BitcoinTestFramework):
|
||||
def set_test_params(self):
|
||||
self.num_nodes = 2
|
||||
self.extra_args = [["-rest", "-blockfilterindex=1"], []]
|
||||
self.extra_args = [["-rest", "-blockfilterindex=1", "-txindex"], []]
|
||||
# whitelist peers to speed up tx relay / mempool sync
|
||||
self.noban_tx_relay = True
|
||||
|
||||
@@ -60,14 +62,14 @@ class RESTTest (BitcoinTestFramework):
|
||||
self,
|
||||
uri: str,
|
||||
http_method: str = 'GET',
|
||||
req_type: ReqType = ReqType.JSON,
|
||||
req_type: typing.Optional[ReqType] = ReqType.JSON,
|
||||
body: str = '',
|
||||
status: int = 200,
|
||||
ret_type: RetType = RetType.JSON,
|
||||
query_params: typing.Union[dict[str, typing.Any], str, None] = None,
|
||||
) -> typing.Union[http.client.HTTPResponse, bytes, str, None]:
|
||||
rest_uri = '/rest' + uri
|
||||
if req_type in ReqType:
|
||||
if isinstance(req_type, ReqType):
|
||||
rest_uri += f'.{req_type.name.lower()}'
|
||||
if query_params:
|
||||
if isinstance(query_params, str):
|
||||
@@ -304,7 +306,7 @@ class RESTTest (BitcoinTestFramework):
|
||||
expected_filter = {
|
||||
'basic block filter index': {'synced': True, 'best_block_height': 208},
|
||||
}
|
||||
self.wait_until(lambda: self.nodes[0].getindexinfo() == expected_filter)
|
||||
self.wait_until(lambda: self.nodes[0].getindexinfo("basic block filter index") == expected_filter)
|
||||
json_obj = self.test_rest_request(f"/headers/{bb_hash}", query_params={"count": 5})
|
||||
assert_equal(len(json_obj), 5) # now we should have 5 header objects
|
||||
json_obj = self.test_rest_request(f"/blockfilterheaders/basic/{bb_hash}", query_params={"count": 5})
|
||||
@@ -523,5 +525,121 @@ class RESTTest (BitcoinTestFramework):
|
||||
resp = self.test_rest_request(f"/deploymentinfo/{INVALID_PARAM}", ret_type=RetType.OBJ, status=400)
|
||||
assert_equal(resp.read().decode('utf-8').rstrip(), f"Invalid hash: {INVALID_PARAM}")
|
||||
|
||||
self.log.info("Test Cache-Control headers on REST responses")
|
||||
|
||||
blockhash = self.nodes[0].getbestblockhash()
|
||||
height = self.nodes[0].getblockcount()
|
||||
immutable = "public, immutable, max-age=86400"
|
||||
no_store = "no-store"
|
||||
|
||||
def assert_cache_control(
|
||||
uri: str,
|
||||
expected: str,
|
||||
*,
|
||||
req_type: typing.Optional[ReqType] = ReqType.JSON,
|
||||
status: int = 200,
|
||||
query_params: typing.Union[dict[str, typing.Any], str, None] = None,
|
||||
) -> bytes:
|
||||
"""Assert `uri` has expected Cache-Control and return the response body."""
|
||||
response = self.test_rest_request(
|
||||
uri,
|
||||
req_type=req_type,
|
||||
status=status,
|
||||
ret_type=RetType.OBJ,
|
||||
query_params=query_params,
|
||||
)
|
||||
assert isinstance(response, http.client.HTTPResponse)
|
||||
assert_equal(response.getheader("Cache-Control"), expected)
|
||||
return response.read()
|
||||
|
||||
# Immutable endpoints
|
||||
immutable_block = assert_cache_control(f"/block/{blockhash}", immutable, req_type=ReqType.BIN)
|
||||
immutable_spenttxouts = assert_cache_control(f"/spenttxouts/{blockhash}", immutable)
|
||||
assert_cache_control(f"/spenttxouts/{blockhash}", immutable, req_type=ReqType.BIN)
|
||||
assert_cache_control(f"/spenttxouts/{blockhash}", immutable, req_type=ReqType.HEX)
|
||||
immutable_blockfilter = assert_cache_control(
|
||||
f"/blockfilter/basic/{blockhash}", immutable, req_type=ReqType.JSON
|
||||
)
|
||||
assert_cache_control(f"/blockfilter/basic/{blockhash}", immutable, req_type=ReqType.BIN)
|
||||
assert_cache_control(f"/blockfilter/basic/{blockhash}", immutable, req_type=ReqType.HEX)
|
||||
|
||||
# Mutable endpoints are not stored until validator support is available.
|
||||
mutable_block = assert_cache_control(f"/block/{blockhash}", no_store)
|
||||
assert_cache_control(f"/block/notxdetails/{blockhash}", no_store)
|
||||
mutable_headers = assert_cache_control(
|
||||
f"/headers/{blockhash}", no_store, req_type=ReqType.JSON, query_params={"count": 2}
|
||||
)
|
||||
assert_cache_control(f"/headers/{blockhash}", no_store, req_type=ReqType.BIN, query_params={"count": 1})
|
||||
assert_cache_control(f"/headers/{blockhash}", no_store, req_type=ReqType.HEX, query_params={"count": 1})
|
||||
assert_cache_control(f"/blockfilterheaders/basic/{blockhash}", no_store, req_type=ReqType.JSON, query_params={"count": 1})
|
||||
assert_cache_control(f"/blockfilterheaders/basic/{blockhash}", no_store, req_type=ReqType.BIN, query_params={"count": 1})
|
||||
assert_cache_control(f"/blockfilterheaders/basic/{blockhash}", no_store, req_type=ReqType.HEX, query_params={"count": 1})
|
||||
assert_cache_control(f"/blockhashbyheight/{height}", no_store)
|
||||
assert_cache_control(f"/blockhashbyheight/{height}", no_store, req_type=ReqType.BIN)
|
||||
assert_cache_control(f"/blockhashbyheight/{height}", no_store, req_type=ReqType.HEX)
|
||||
|
||||
# Dynamic endpoints
|
||||
mutable_chaininfo = assert_cache_control("/chaininfo", no_store)
|
||||
assert_cache_control("/mempool/info", no_store)
|
||||
mutable_deploymentinfo = assert_cache_control("/deploymentinfo", no_store)
|
||||
immutable_deploymentinfo = assert_cache_control(f"/deploymentinfo/{blockhash}", immutable)
|
||||
|
||||
cache_tx = self.wallet.send_self_transfer(from_node=self.nodes[0])
|
||||
mempool_txid = cache_tx["txid"]
|
||||
cache_utxo = cache_tx["new_utxo"]
|
||||
cache_utxo_path = f"/getutxos/{mempool_txid}-{cache_utxo['vout']}"
|
||||
|
||||
mempool_tx_response = assert_cache_control(f"/tx/{mempool_txid}", no_store, req_type=ReqType.JSON)
|
||||
mempool_tx = json.loads(mempool_tx_response, parse_float=Decimal)
|
||||
assert "blockhash" not in mempool_tx
|
||||
assert_cache_control(f"/tx/{mempool_txid}", no_store, req_type=ReqType.BIN)
|
||||
assert_cache_control(f"/tx/{mempool_txid}", no_store, req_type=ReqType.HEX)
|
||||
|
||||
self.generate(self.nodes[0], 1)
|
||||
sync_txindex(self, self.nodes[0])
|
||||
|
||||
self.log.info("Test Cache-Control response behavior across a newly mined block")
|
||||
assert_equal(immutable_block, assert_cache_control(f"/block/{blockhash}", immutable, req_type=ReqType.BIN))
|
||||
assert_equal(immutable_spenttxouts, assert_cache_control(f"/spenttxouts/{blockhash}", immutable))
|
||||
assert_equal(
|
||||
immutable_blockfilter,
|
||||
assert_cache_control(f"/blockfilter/basic/{blockhash}", immutable, req_type=ReqType.JSON),
|
||||
)
|
||||
assert_equal(immutable_deploymentinfo, assert_cache_control(f"/deploymentinfo/{blockhash}", immutable))
|
||||
|
||||
# no-store responses need not change after every block. Only compare responses
|
||||
# that are guaranteed to incorporate this active chain extension.
|
||||
assert_not_equal(mutable_block, assert_cache_control(f"/block/{blockhash}", no_store))
|
||||
assert_not_equal(
|
||||
mutable_headers,
|
||||
assert_cache_control(f"/headers/{blockhash}", no_store, req_type=ReqType.JSON, query_params={"count": 2}),
|
||||
)
|
||||
assert_not_equal(mutable_chaininfo, assert_cache_control("/chaininfo", no_store))
|
||||
assert_not_equal(mutable_deploymentinfo, assert_cache_control("/deploymentinfo", no_store))
|
||||
|
||||
confirmed_tx_response = assert_cache_control(f"/tx/{mempool_txid}", no_store, req_type=ReqType.JSON)
|
||||
assert_not_equal(mempool_tx_response, confirmed_tx_response)
|
||||
confirmed_tx = json.loads(confirmed_tx_response, parse_float=Decimal)
|
||||
assert_equal(confirmed_tx["txid"], mempool_txid)
|
||||
assert "blockhash" in confirmed_tx
|
||||
assert_cache_control(f"/tx/{mempool_txid}", no_store, req_type=ReqType.BIN)
|
||||
assert_cache_control(f"/tx/{mempool_txid}", no_store, req_type=ReqType.HEX)
|
||||
assert_cache_control(cache_utxo_path, no_store, req_type=ReqType.JSON)
|
||||
assert_cache_control(cache_utxo_path, no_store, req_type=ReqType.BIN)
|
||||
assert_cache_control(cache_utxo_path, no_store, req_type=ReqType.HEX)
|
||||
|
||||
self.log.info("Test Cache-Control headers on REST error responses")
|
||||
assert_cache_control(f"/block/{blockhash}.invalid", no_store, status=400, req_type=None)
|
||||
assert_cache_control(f"/tx/{INVALID_PARAM}", no_store, status=400)
|
||||
assert_cache_control(f"/deploymentinfo/{INVALID_PARAM}", no_store, status=400)
|
||||
assert_cache_control("/blockhashbyheight/999999999", no_store, status=404)
|
||||
assert_cache_control(f"/block/{UNKNOWN_PARAM}", no_store, status=404)
|
||||
assert_cache_control(f"/tx/{'f' * 64}", no_store, status=404)
|
||||
assert_cache_control("", no_store, status=404, query_params={"x": 1}, req_type=None)
|
||||
assert_cache_control("/tx", no_store, status=404, req_type=None)
|
||||
assert_cache_control("/does-not-exist", no_store, status=404, req_type=None)
|
||||
assert_cache_control("/mempool/not-a-valid-path", no_store, status=400)
|
||||
assert_cache_control(f"/deploymentinfo/{non_existing_blockhash}", no_store, status=400)
|
||||
|
||||
if __name__ == '__main__':
|
||||
RESTTest(__file__).main()
|
||||
|
||||
Reference in New Issue
Block a user