mirror of
https://github.com/bitcoinresearchkit/brk.git
synced 2026-04-26 15:49:58 -07:00
global: snapshot
This commit is contained in:
@@ -6,11 +6,11 @@ use axum::{
|
||||
http::{HeaderMap, StatusCode, Uri},
|
||||
response::{IntoResponse, Response},
|
||||
};
|
||||
use brk_error::Result;
|
||||
use brk_types::{Format, MetricSelection, Output};
|
||||
use quick_cache::sync::GuardResult;
|
||||
|
||||
use crate::{
|
||||
Result,
|
||||
api::metrics::{CACHE_CONTROL, MAX_WEIGHT},
|
||||
extended::HeaderMapExtended,
|
||||
};
|
||||
@@ -18,22 +18,10 @@ use crate::{
|
||||
use super::AppState;
|
||||
|
||||
pub async fn handler(
|
||||
uri: Uri,
|
||||
headers: HeaderMap,
|
||||
query: Query<MetricSelection>,
|
||||
State(state): State<AppState>,
|
||||
) -> Response {
|
||||
match req_to_response_res(uri, headers, query, state).await {
|
||||
Ok(response) => response,
|
||||
Err(error) => (StatusCode::INTERNAL_SERVER_ERROR, error.to_string()).into_response(),
|
||||
}
|
||||
}
|
||||
|
||||
async fn req_to_response_res(
|
||||
uri: Uri,
|
||||
headers: HeaderMap,
|
||||
Query(params): Query<MetricSelection>,
|
||||
AppState { query, cache, .. }: AppState,
|
||||
State(AppState { query, cache, .. }): State<AppState>,
|
||||
) -> Result<Response> {
|
||||
// Phase 1: Search and resolve metadata (cheap)
|
||||
let resolved = query.run(move |q| q.resolve(params, MAX_WEIGHT)).await?;
|
||||
|
||||
@@ -6,11 +6,11 @@ use axum::{
|
||||
http::{HeaderMap, StatusCode, Uri},
|
||||
response::{IntoResponse, Response},
|
||||
};
|
||||
use brk_error::Result;
|
||||
use brk_types::{Format, MetricSelection, Output};
|
||||
use quick_cache::sync::GuardResult;
|
||||
|
||||
use crate::{
|
||||
Result,
|
||||
api::metrics::{CACHE_CONTROL, MAX_WEIGHT},
|
||||
extended::HeaderMapExtended,
|
||||
};
|
||||
@@ -18,22 +18,10 @@ use crate::{
|
||||
use super::AppState;
|
||||
|
||||
pub async fn handler(
|
||||
uri: Uri,
|
||||
headers: HeaderMap,
|
||||
query: Query<MetricSelection>,
|
||||
State(state): State<AppState>,
|
||||
) -> Response {
|
||||
match req_to_response_res(uri, headers, query, state).await {
|
||||
Ok(response) => response,
|
||||
Err(error) => (StatusCode::INTERNAL_SERVER_ERROR, error.to_string()).into_response(),
|
||||
}
|
||||
}
|
||||
|
||||
async fn req_to_response_res(
|
||||
uri: Uri,
|
||||
headers: HeaderMap,
|
||||
Query(params): Query<MetricSelection>,
|
||||
AppState { query, cache, .. }: AppState,
|
||||
State(AppState { query, cache, .. }): State<AppState>,
|
||||
) -> Result<Response> {
|
||||
// Phase 1: Search and resolve metadata (cheap)
|
||||
let resolved = query.run(move |q| q.resolve(params, MAX_WEIGHT)).await?;
|
||||
|
||||
@@ -6,11 +6,11 @@ use axum::{
|
||||
http::{HeaderMap, StatusCode, Uri},
|
||||
response::{IntoResponse, Response},
|
||||
};
|
||||
use brk_error::Result;
|
||||
use brk_types::{Format, MetricSelection, OutputLegacy};
|
||||
use quick_cache::sync::GuardResult;
|
||||
|
||||
use crate::{
|
||||
Result,
|
||||
api::metrics::{CACHE_CONTROL, MAX_WEIGHT},
|
||||
extended::HeaderMapExtended,
|
||||
};
|
||||
@@ -18,22 +18,10 @@ use crate::{
|
||||
use super::AppState;
|
||||
|
||||
pub async fn handler(
|
||||
uri: Uri,
|
||||
headers: HeaderMap,
|
||||
query: Query<MetricSelection>,
|
||||
State(state): State<AppState>,
|
||||
) -> Response {
|
||||
match req_to_response_res(uri, headers, query, state).await {
|
||||
Ok(response) => response,
|
||||
Err(error) => (StatusCode::INTERNAL_SERVER_ERROR, error.to_string()).into_response(),
|
||||
}
|
||||
}
|
||||
|
||||
async fn req_to_response_res(
|
||||
uri: Uri,
|
||||
headers: HeaderMap,
|
||||
Query(params): Query<MetricSelection>,
|
||||
AppState { query, cache, .. }: AppState,
|
||||
State(AppState { query, cache, .. }): State<AppState>,
|
||||
) -> Result<Response> {
|
||||
// Phase 1: Search and resolve metadata (cheap)
|
||||
let resolved = query.run(move |q| q.resolve(params, MAX_WEIGHT)).await?;
|
||||
|
||||
@@ -170,6 +170,7 @@ impl ApiMetricsRoutes for ApiRouter<AppState> {
|
||||
state,
|
||||
)
|
||||
.await
|
||||
.into_response()
|
||||
},
|
||||
|op| op
|
||||
.id("get_metric")
|
||||
@@ -188,7 +189,9 @@ impl ApiMetricsRoutes for ApiRouter<AppState> {
|
||||
.api_route(
|
||||
"/api/metrics/bulk",
|
||||
get_with(
|
||||
bulk::handler,
|
||||
|uri, headers, query, state| async move {
|
||||
bulk::handler(uri, headers, query, state).await.into_response()
|
||||
},
|
||||
|op| op
|
||||
.id("get_metrics")
|
||||
.metrics_tag()
|
||||
@@ -225,7 +228,9 @@ impl ApiMetricsRoutes for ApiRouter<AppState> {
|
||||
Metrics::from(split.collect::<Vec<_>>().join(separator)),
|
||||
range,
|
||||
));
|
||||
legacy::handler(uri, headers, Query(params), state).await
|
||||
legacy::handler(uri, headers, Query(params), state)
|
||||
.await
|
||||
.into_response()
|
||||
},
|
||||
|op| op
|
||||
.metrics_tag()
|
||||
@@ -250,7 +255,9 @@ impl ApiMetricsRoutes for ApiRouter<AppState> {
|
||||
state: State<AppState>|
|
||||
-> Response {
|
||||
let params: MetricSelection = params.into();
|
||||
legacy::handler(uri, headers, Query(params), state).await
|
||||
legacy::handler(uri, headers, Query(params), state)
|
||||
.await
|
||||
.into_response()
|
||||
},
|
||||
|op| op
|
||||
.metrics_tag()
|
||||
|
||||
@@ -48,7 +48,7 @@ impl ApiRoutes for ApiRouter<AppState> {
|
||||
.add_server_routes()
|
||||
.route("/api/server", get(Redirect::temporary("/api#tag/server")))
|
||||
.api_route(
|
||||
"/api.json",
|
||||
"/openapi.json",
|
||||
get_with(
|
||||
async |headers: HeaderMap,
|
||||
Extension(api): Extension<Arc<OpenApi>>|
|
||||
@@ -62,7 +62,7 @@ impl ApiRoutes for ApiRouter<AppState> {
|
||||
),
|
||||
)
|
||||
.api_route(
|
||||
"/api.trimmed.json",
|
||||
"/api.json",
|
||||
get_with(
|
||||
async |headers: HeaderMap,
|
||||
Extension(api_trimmed): Extension<Arc<String>>|
|
||||
@@ -72,12 +72,13 @@ impl ApiRoutes for ApiRouter<AppState> {
|
||||
Response::static_json(&headers, &value)
|
||||
},
|
||||
|op| {
|
||||
op.id("get_openapi_trimmed")
|
||||
op.id("get_api")
|
||||
.server_tag()
|
||||
.summary("Trimmed OpenAPI specification")
|
||||
.summary("Compact OpenAPI specification")
|
||||
.description(
|
||||
"Compact OpenAPI specification optimized for LLM consumption. \
|
||||
Removes redundant fields while preserving essential API information.",
|
||||
Removes redundant fields while preserving essential API information. \
|
||||
Full spec available at `/openapi.json`.",
|
||||
)
|
||||
.ok_response::<serde_json::Value>()
|
||||
},
|
||||
|
||||
@@ -29,7 +29,7 @@ pub fn create_openapi() -> OpenApi {
|
||||
- **Metrics**: Thousands of time-series metrics across multiple indexes (date, block height, etc.)
|
||||
- **[Mempool.space](https://mempool.space/docs/api/rest) compatible** (WIP): Most non-metrics endpoints follow the mempool.space API format
|
||||
- **Multiple formats**: JSON and CSV output
|
||||
- **LLM-optimized**: Compact OpenAPI spec at [`/api.trimmed.json`](/api.trimmed.json) for AI tools
|
||||
- **LLM-optimized**: Compact OpenAPI spec at [`/api.json`](/api.json) for AI tools (full spec at [`/openapi.json`](/openapi.json))
|
||||
|
||||
### Client Libraries
|
||||
|
||||
|
||||
@@ -18,7 +18,7 @@
|
||||
|
||||
<script>
|
||||
Scalar.createApiReference("#app", {
|
||||
url: "/api.json",
|
||||
url: "/openapi.json",
|
||||
hideClientButton: true,
|
||||
telemetry: false,
|
||||
// showToolbar: "never",
|
||||
|
||||
58
crates/brk_server/src/error.rs
Normal file
58
crates/brk_server/src/error.rs
Normal file
@@ -0,0 +1,58 @@
|
||||
use axum::{
|
||||
http::StatusCode,
|
||||
response::{IntoResponse, Response},
|
||||
};
|
||||
use brk_error::Error as BrkError;
|
||||
|
||||
/// Server result type with Error that implements IntoResponse.
|
||||
pub type Result<T> = std::result::Result<T, Error>;
|
||||
|
||||
/// Server error type that maps to HTTP status codes.
|
||||
pub struct Error(StatusCode, String);
|
||||
|
||||
impl Error {
|
||||
pub fn bad_request(msg: impl Into<String>) -> Self {
|
||||
Self(StatusCode::BAD_REQUEST, msg.into())
|
||||
}
|
||||
|
||||
pub fn forbidden(msg: impl Into<String>) -> Self {
|
||||
Self(StatusCode::FORBIDDEN, msg.into())
|
||||
}
|
||||
|
||||
pub fn not_found(msg: impl Into<String>) -> Self {
|
||||
Self(StatusCode::NOT_FOUND, msg.into())
|
||||
}
|
||||
|
||||
pub fn internal(msg: impl Into<String>) -> Self {
|
||||
Self(StatusCode::INTERNAL_SERVER_ERROR, msg.into())
|
||||
}
|
||||
}
|
||||
|
||||
impl From<BrkError> for Error {
|
||||
fn from(e: BrkError) -> Self {
|
||||
let status = match &e {
|
||||
BrkError::InvalidTxid
|
||||
| BrkError::InvalidNetwork
|
||||
| BrkError::InvalidAddress
|
||||
| BrkError::UnsupportedType(_)
|
||||
| BrkError::Parse(_)
|
||||
| BrkError::NoMetrics
|
||||
| BrkError::MetricUnsupportedIndex { .. }
|
||||
| BrkError::WeightExceeded { .. } => StatusCode::BAD_REQUEST,
|
||||
|
||||
BrkError::UnknownAddress
|
||||
| BrkError::UnknownTxid
|
||||
| BrkError::NotFound(_)
|
||||
| BrkError::MetricNotFound { .. } => StatusCode::NOT_FOUND,
|
||||
|
||||
_ => StatusCode::INTERNAL_SERVER_ERROR,
|
||||
};
|
||||
Self(status, e.to_string())
|
||||
}
|
||||
}
|
||||
|
||||
impl IntoResponse for Error {
|
||||
fn into_response(self) -> Response {
|
||||
(self.0, self.1).into_response()
|
||||
}
|
||||
}
|
||||
@@ -7,30 +7,30 @@ use std::{
|
||||
use axum::{
|
||||
body::Body,
|
||||
extract::{self, State},
|
||||
http::{HeaderMap, StatusCode},
|
||||
response::{IntoResponse, Response},
|
||||
http::HeaderMap,
|
||||
response::Response,
|
||||
};
|
||||
use brk_error::Result;
|
||||
use quick_cache::sync::GuardResult;
|
||||
use tracing::{error, info};
|
||||
|
||||
use crate::{
|
||||
AppState, EMBEDDED_WEBSITE, HeaderMapExtended, ModifiedState, ResponseExtended, WebsiteSource,
|
||||
AppState, EMBEDDED_WEBSITE, Error, HeaderMapExtended, ModifiedState, ResponseExtended, Result,
|
||||
WebsiteSource,
|
||||
};
|
||||
|
||||
pub async fn file_handler(
|
||||
headers: HeaderMap,
|
||||
State(state): State<AppState>,
|
||||
path: extract::Path<String>,
|
||||
) -> Response {
|
||||
) -> Result<Response> {
|
||||
any_handler(headers, state, Some(path.0))
|
||||
}
|
||||
|
||||
pub async fn index_handler(headers: HeaderMap, State(state): State<AppState>) -> Response {
|
||||
pub async fn index_handler(headers: HeaderMap, State(state): State<AppState>) -> Result<Response> {
|
||||
any_handler(headers, state, None)
|
||||
}
|
||||
|
||||
fn any_handler(headers: HeaderMap, state: AppState, path: Option<String>) -> Response {
|
||||
fn any_handler(headers: HeaderMap, state: AppState, path: Option<String>) -> Result<Response> {
|
||||
match &state.website {
|
||||
WebsiteSource::Disabled => unreachable!("routes not added when disabled"),
|
||||
WebsiteSource::Embedded => embedded_handler(&state, path),
|
||||
@@ -92,7 +92,7 @@ fn build_response(state: &AppState, path: &Path, content: Vec<u8>, cache_key: &s
|
||||
response
|
||||
}
|
||||
|
||||
fn embedded_handler(state: &AppState, path: Option<String>) -> Response {
|
||||
fn embedded_handler(state: &AppState, path: Option<String>) -> Result<Response> {
|
||||
let path = path.unwrap_or_else(|| "index.html".to_string());
|
||||
let sanitized = sanitize_path(&path);
|
||||
|
||||
@@ -113,17 +113,15 @@ fn embedded_handler(state: &AppState, path: Option<String>) -> Response {
|
||||
});
|
||||
|
||||
let Some(file) = file else {
|
||||
let response: Response<Body> =
|
||||
(StatusCode::NOT_FOUND, "File not found".to_string()).into_response();
|
||||
return response;
|
||||
return Err(Error::not_found("File not found"));
|
||||
};
|
||||
|
||||
build_response(
|
||||
Ok(build_response(
|
||||
state,
|
||||
Path::new(file.path()),
|
||||
file.contents().to_vec(),
|
||||
&file.path().to_string_lossy(),
|
||||
)
|
||||
))
|
||||
}
|
||||
|
||||
fn filesystem_handler(
|
||||
@@ -131,7 +129,7 @@ fn filesystem_handler(
|
||||
state: &AppState,
|
||||
files_path: &Path,
|
||||
path: Option<String>,
|
||||
) -> Response {
|
||||
) -> Result<Response> {
|
||||
let path = if let Some(path) = path {
|
||||
let sanitized = sanitize_path(&path);
|
||||
let mut path = files_path.join(&sanitized);
|
||||
@@ -145,9 +143,7 @@ fn filesystem_handler(
|
||||
let allowed = canonical.starts_with(&canonical_base)
|
||||
|| project_root.is_some_and(|root| canonical.starts_with(root));
|
||||
if !allowed {
|
||||
let response: Response<Body> =
|
||||
(StatusCode::FORBIDDEN, "Access denied".to_string()).into_response();
|
||||
return response;
|
||||
return Err(Error::forbidden("Access denied"));
|
||||
}
|
||||
}
|
||||
|
||||
@@ -162,12 +158,7 @@ fn filesystem_handler(
|
||||
// SPA fallback
|
||||
if !path.exists() || path.is_dir() {
|
||||
if path.extension().is_some() {
|
||||
let response: Response<Body> = (
|
||||
StatusCode::INTERNAL_SERVER_ERROR,
|
||||
"File doesn't exist".to_string(),
|
||||
)
|
||||
.into_response();
|
||||
return response;
|
||||
return Err(Error::not_found("File doesn't exist"));
|
||||
} else {
|
||||
path = files_path.join("index.html");
|
||||
}
|
||||
@@ -181,14 +172,7 @@ fn filesystem_handler(
|
||||
path_to_response(&headers, state, &path)
|
||||
}
|
||||
|
||||
fn path_to_response(headers: &HeaderMap, state: &AppState, path: &Path) -> Response {
|
||||
match path_to_response_(headers, state, path) {
|
||||
Ok(response) => response,
|
||||
Err(error) => (StatusCode::INTERNAL_SERVER_ERROR, error.to_string()).into_response(),
|
||||
}
|
||||
}
|
||||
|
||||
fn path_to_response_(headers: &HeaderMap, state: &AppState, path: &Path) -> Result<Response> {
|
||||
fn path_to_response(headers: &HeaderMap, state: &AppState, path: &Path) -> Result<Response> {
|
||||
let (modified, date) = headers.check_if_modified_since(path)?;
|
||||
if !cfg!(debug_assertions) && modified == ModifiedState::NotModifiedSince {
|
||||
return Ok(Response::new_not_modified());
|
||||
|
||||
@@ -17,7 +17,6 @@ use axum::{
|
||||
routing::get,
|
||||
serve,
|
||||
};
|
||||
use brk_error::Result;
|
||||
use brk_query::AsyncQuery;
|
||||
use include_dir::{Dir, include_dir};
|
||||
use quick_cache::sync::Cache;
|
||||
@@ -48,12 +47,14 @@ impl WebsiteSource {
|
||||
|
||||
mod api;
|
||||
pub mod cache;
|
||||
mod error;
|
||||
mod extended;
|
||||
mod files;
|
||||
mod state;
|
||||
|
||||
use api::*;
|
||||
pub use cache::{CacheParams, CacheStrategy};
|
||||
pub use error::{Error, Result};
|
||||
use extended::*;
|
||||
use files::FilesRoutes;
|
||||
use state::*;
|
||||
@@ -75,7 +76,7 @@ impl Server {
|
||||
})
|
||||
}
|
||||
|
||||
pub async fn serve(self) -> Result<()> {
|
||||
pub async fn serve(self) -> brk_error::Result<()> {
|
||||
let state = self.0;
|
||||
|
||||
let compression_layer = CompressionLayer::new()
|
||||
|
||||
Reference in New Issue
Block a user