Skip to content
This repository was archived by the owner on Oct 18, 2023. It is now read-only.

Commit fb8b3c2

Browse files
bors[bot]honzasp
andauthored
Merge #305
305: Add HTTP API v1 ("Hrana over HTTP") r=MarinPostma a=honzasp We need to update the HTTP API to support batches and to fix some issues (such as encoding of types). Instead of coming up with a new API, let's just reuse the Hrana structures and semantics. In effect, the new version of the HTTP API is "Hrana over HTTP". This is needed for [the new generation of the client libraries](tursodatabase/libsql-client-ts#10). Co-authored-by: Jan Špaček <patek.mail@gmail.com>
2 parents a558b64 + c50b035 commit fb8b3c2

File tree

6 files changed

+240
-3
lines changed

6 files changed

+240
-3
lines changed

Cargo.lock

Lines changed: 1 addition & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

docs/HTTP_V1_SPEC.md

Lines changed: 70 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,70 @@
1+
# The sqld HTTP API v1 specification ("Hrana over HTTP")
2+
3+
Version 1 of the HTTP API ("Hrana over HTTP") is designed to complement the
4+
WebSocket-based Hrana protocol for use cases that don't require stateful
5+
database connections and for which the additional network rountrip required by
6+
WebSockets relative to HTTP is not necessary.
7+
8+
This API aims to be of production quality and it is primarily intended to be
9+
consumed by client libraries. It does not deprecate or replace the "version 0"
10+
of the HTTP API, which is designed to be quick and easy for users who send HTTP
11+
requests manually (for example using `curl` or by directly using an HTTP
12+
library).
13+
14+
## Overview
15+
16+
This HTTP API uses data structures and semantics from the Hrana protocol;
17+
versions of the HTTP API are intended to correspond to versions of the Hrana
18+
protocol, so HTTP API v1 corresponds to the `hrana1` version of Hrana.
19+
20+
Endpoints in the HTTP API correspond to requests in Hrana. Each request is
21+
executed as if a fresh Hrana stream was opened for the request.
22+
23+
All request and response bodies are encoded in JSON, with content type
24+
`application/json`.
25+
26+
## Execute a statement
27+
28+
```
29+
POST /v1/execute
30+
31+
-> {
32+
"stmt": Stmt,
33+
}
34+
35+
<- {
36+
"result": StmtResult,
37+
}
38+
```
39+
40+
The `execute` endpoint receives a statement and returns the result of executing
41+
the statement. The `Stmt` and `StmtResult` structures are from the Hrana
42+
protocol. The semantics of this endpoint is the same as the `execute` request in
43+
Hrana.
44+
45+
## Execute a batch
46+
47+
```
48+
POST /v1/batch
49+
50+
-> {
51+
"batch": Batch,
52+
}
53+
54+
<- {
55+
"result": BatchResult,
56+
}
57+
```
58+
59+
The `batch` endpoint receives a batch and returns the result of executing the
60+
statement. The `Batch` and `BatchResult` structures are from the Hrana protocol.
61+
The semantics of this endpoint is the same as the `batch` request in Hrana.
62+
63+
## Errors
64+
65+
Successful responses are indicated by a HTTP status code in range [200, 300).
66+
Errors are indicated with HTTP status codes in range [400, 600), and the error
67+
responses should have the format of `Error` from the Hrana protocol. However,
68+
the clients should be able to handle error responses that don't correspond to
69+
this format; in particular, the server may produce some error responses with the
70+
error message as plain text.

sqld/Cargo.toml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -39,7 +39,7 @@ rusqlite = { version = "0.29.0", git = "https://github.com/psarna/rusqlite", rev
3939
"column_decltype"
4040
] }
4141
serde = { version = "1.0.149", features = ["derive", "rc"] }
42-
serde_json = "1.0.91"
42+
serde_json = { version = "1.0.91", features = ["preserve_order"] }
4343
smallvec = "1.10.0"
4444
sqld-libsql-bindings = { version = "0", path = "../sqld-libsql-bindings" }
4545
sqlite3-parser = { version = "0.6.0", default-features = false, features = [ "YYNOERRORRECOVERY" ] }

sqld/src/http/hrana_over_http.rs

Lines changed: 157 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,157 @@
1+
use anyhow::{anyhow, Context, Result};
2+
use serde::{de::DeserializeOwned, Deserialize, Serialize};
3+
use std::future::Future;
4+
use std::sync::Arc;
5+
6+
use crate::database::service::DbFactory;
7+
use crate::database::Database;
8+
use crate::{batch, hrana};
9+
10+
#[derive(thiserror::Error, Debug)]
11+
enum ResponseError {
12+
#[error("Could not parse request body: {source}")]
13+
BadRequestBody { source: serde_json::Error },
14+
15+
#[error(transparent)]
16+
Stmt(batch::StmtError),
17+
#[error(transparent)]
18+
Batch(batch::BatchError),
19+
}
20+
21+
pub async fn handle_index(
22+
_req: hyper::Request<hyper::Body>,
23+
) -> Result<hyper::Response<hyper::Body>> {
24+
let body = "This is sqld HTTP API v1 (\"Hrana over HTTP\")";
25+
let body = hyper::Body::from(body);
26+
Ok(hyper::Response::builder()
27+
.header("content-type", "text/plain")
28+
.body(body)
29+
.unwrap())
30+
}
31+
32+
pub async fn handle_execute(
33+
db_factory: Arc<dyn DbFactory>,
34+
req: hyper::Request<hyper::Body>,
35+
) -> Result<hyper::Response<hyper::Body>> {
36+
#[derive(Debug, Deserialize)]
37+
struct ReqBody {
38+
stmt: batch::proto::Stmt,
39+
}
40+
41+
#[derive(Debug, Serialize)]
42+
struct RespBody {
43+
result: batch::proto::StmtResult,
44+
}
45+
46+
handle_request(db_factory, req, |db, req_body: ReqBody| async move {
47+
batch::execute_stmt(&*db, &req_body.stmt)
48+
.await
49+
.map(|result| RespBody { result })
50+
.map_err(|err| match err.downcast::<batch::StmtError>() {
51+
Ok(stmt_err) => anyhow!(ResponseError::Stmt(stmt_err)),
52+
Err(err) => err,
53+
})
54+
.context("Could not execute statement")
55+
})
56+
.await
57+
}
58+
59+
pub async fn handle_batch(
60+
db_factory: Arc<dyn DbFactory>,
61+
req: hyper::Request<hyper::Body>,
62+
) -> Result<hyper::Response<hyper::Body>> {
63+
#[derive(Debug, Deserialize)]
64+
struct ReqBody {
65+
batch: batch::proto::Batch,
66+
}
67+
68+
#[derive(Debug, Serialize)]
69+
struct RespBody {
70+
result: batch::proto::BatchResult,
71+
}
72+
73+
handle_request(db_factory, req, |db, req_body: ReqBody| async move {
74+
batch::execute_batch(&*db, &req_body.batch)
75+
.await
76+
.map(|result| RespBody { result })
77+
.map_err(|err| match err.downcast::<batch::BatchError>() {
78+
Ok(batch_err) => anyhow!(ResponseError::Batch(batch_err)),
79+
Err(err) => err,
80+
})
81+
.context("Could not execute batch")
82+
})
83+
.await
84+
}
85+
86+
async fn handle_request<ReqBody, RespBody, F, Fut>(
87+
db_factory: Arc<dyn DbFactory>,
88+
req: hyper::Request<hyper::Body>,
89+
f: F,
90+
) -> Result<hyper::Response<hyper::Body>>
91+
where
92+
ReqBody: DeserializeOwned,
93+
RespBody: Serialize,
94+
F: FnOnce(Arc<dyn Database>, ReqBody) -> Fut,
95+
Fut: Future<Output = Result<RespBody>>,
96+
{
97+
let res: Result<_> = async move {
98+
let req_body = hyper::body::to_bytes(req.into_body()).await?;
99+
let req_body = serde_json::from_slice(&req_body)
100+
.map_err(|e| ResponseError::BadRequestBody { source: e })?;
101+
102+
let db = db_factory
103+
.create()
104+
.await
105+
.context("Could not create a database connection")?;
106+
let resp_body = f(db, req_body).await?;
107+
108+
Ok(json_response(hyper::StatusCode::OK, &resp_body))
109+
}
110+
.await;
111+
112+
Ok(match res {
113+
Ok(resp) => resp,
114+
Err(err) => error_response(err.downcast::<ResponseError>()?),
115+
})
116+
}
117+
118+
fn error_response(err: ResponseError) -> hyper::Response<hyper::Body> {
119+
use batch::{BatchError, StmtError};
120+
let status = match &err {
121+
ResponseError::BadRequestBody { .. } => hyper::StatusCode::BAD_REQUEST,
122+
ResponseError::Stmt(err) => match err {
123+
StmtError::SqlParse { .. }
124+
| StmtError::SqlNoStmt
125+
| StmtError::SqlManyStmts
126+
| StmtError::ArgsInvalid { .. }
127+
| StmtError::SqlInputError { .. } => hyper::StatusCode::BAD_REQUEST,
128+
StmtError::ArgsBothPositionalAndNamed => hyper::StatusCode::NOT_IMPLEMENTED,
129+
StmtError::TransactionTimeout | StmtError::TransactionBusy => {
130+
hyper::StatusCode::SERVICE_UNAVAILABLE
131+
}
132+
StmtError::SqliteError { .. } => hyper::StatusCode::INTERNAL_SERVER_ERROR,
133+
},
134+
ResponseError::Batch(err) => match err {
135+
BatchError::CondBadStep => hyper::StatusCode::BAD_REQUEST,
136+
},
137+
};
138+
139+
json_response(
140+
status,
141+
&hrana::proto::Error {
142+
message: err.to_string(),
143+
},
144+
)
145+
}
146+
147+
fn json_response<T: Serialize>(
148+
status: hyper::StatusCode,
149+
body: &T,
150+
) -> hyper::Response<hyper::Body> {
151+
let body = serde_json::to_vec(body).unwrap();
152+
hyper::Response::builder()
153+
.status(status)
154+
.header("content-type", "application/json")
155+
.body(hyper::Body::from(body))
156+
.unwrap()
157+
}

sqld/src/http/mod.rs

Lines changed: 10 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,4 @@
1+
mod hrana_over_http;
12
mod types;
23

34
use std::future::poll_fn;
@@ -22,6 +23,7 @@ use tower_http::{compression::CompressionLayer, cors};
2223
use tracing::{Level, Span};
2324

2425
use crate::auth::Auth;
26+
use crate::database::service::DbFactory;
2527
use crate::error::Error;
2628
use crate::hrana;
2729
use crate::http::types::HttpQuery;
@@ -230,6 +232,7 @@ async fn handle_request(
230232
req: Request<Body>,
231233
sender: mpsc::Sender<Message>,
232234
upgrade_tx: mpsc::Sender<hrana::Upgrade>,
235+
db_factory: Arc<dyn DbFactory>,
233236
enable_console: bool,
234237
) -> anyhow::Result<Response<Body>> {
235238
if hyper_tungstenite::is_upgrade_request(&req) {
@@ -249,6 +252,9 @@ async fn handle_request(
249252
(&Method::GET, "/version") => Ok(handle_version()),
250253
(&Method::GET, "/console") if enable_console => show_console().await,
251254
(&Method::GET, "/health") => Ok(handle_health()),
255+
(&Method::GET, "/v1") => hrana_over_http::handle_index(req).await,
256+
(&Method::POST, "/v1/execute") => hrana_over_http::handle_execute(db_factory, req).await,
257+
(&Method::POST, "/v1/batch") => hrana_over_http::handle_batch(db_factory, req).await,
252258
_ => Ok(Response::builder().status(404).body(Body::empty()).unwrap()),
253259
}
254260
}
@@ -261,7 +267,8 @@ fn handle_version() -> Response<Body> {
261267
pub async fn run_http<F>(
262268
addr: SocketAddr,
263269
auth: Arc<Auth>,
264-
db_factory: F,
270+
db_factory_service: F,
271+
db_factory: Arc<dyn DbFactory>,
265272
upgrade_tx: mpsc::Sender<hrana::Upgrade>,
266273
enable_console: bool,
267274
idle_shutdown_layer: Option<IdleShutdownLayer>,
@@ -306,14 +313,15 @@ where
306313
req,
307314
sender.clone(),
308315
upgrade_tx.clone(),
316+
db_factory.clone(),
309317
enable_console,
310318
)
311319
});
312320

313321
let server = hyper::server::Server::bind(&addr).serve(tower::make::Shared::new(service));
314322

315323
tokio::spawn(async move {
316-
let mut pool = pool::Builder::new().build(db_factory, ());
324+
let mut pool = pool::Builder::new().build(db_factory_service, ());
317325
while let Some(Message { queries, resp }) = receiver.recv().await {
318326
if let Err(e) = poll_fn(|c| pool.poll_ready(c)).await {
319327
tracing::error!("Connection pool error: {e}");

sqld/src/lib.rs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -119,6 +119,7 @@ async fn run_service(
119119
addr,
120120
auth.clone(),
121121
service.clone().map_response(|s| Constant::new(s, 1)),
122+
service.factory.clone(),
122123
upgrade_tx,
123124
config.enable_http_console,
124125
idle_shutdown_layer.clone(),

0 commit comments

Comments
 (0)