convex-rust 0.0.0-stage → 0.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +201 -0
- package/README.md +137 -2
- package/dist/cli/bin.d.ts +2 -0
- package/dist/cli/bin.js +200 -0
- package/dist/cli/build.d.ts +146 -0
- package/dist/cli/build.js +902 -0
- package/dist/cli/demangle.d.ts +7 -0
- package/dist/cli/demangle.js +328 -0
- package/dist/cli/diagnostics.d.ts +34 -0
- package/dist/cli/diagnostics.js +296 -0
- package/dist/cli/generate.d.ts +29 -0
- package/dist/cli/generate.js +82 -0
- package/dist/cli/manifest.d.ts +24 -0
- package/dist/cli/manifest.js +75 -0
- package/dist/cli/rustApi.d.ts +78 -0
- package/dist/cli/rustApi.js +609 -0
- package/dist/cli/scaffold.d.ts +22 -0
- package/dist/cli/scaffold.js +114 -0
- package/dist/cli/shim.d.ts +30 -0
- package/dist/cli/shim.js +140 -0
- package/dist/cli/size.d.ts +17 -0
- package/dist/cli/size.js +73 -0
- package/dist/cli/split.d.ts +20 -0
- package/dist/cli/split.js +140 -0
- package/dist/cli/symbols.d.ts +30 -0
- package/dist/cli/symbols.js +133 -0
- package/dist/cli/toolchain.d.ts +53 -0
- package/dist/cli/toolchain.js +167 -0
- package/dist/cli/validators.d.ts +6 -0
- package/dist/cli/validators.js +66 -0
- package/dist/cli/wasisdk.d.ts +82 -0
- package/dist/cli/wasisdk.js +220 -0
- package/dist/cli/wasmHints.d.ts +29 -0
- package/dist/cli/wasmHints.js +172 -0
- package/dist/runtime/base64.d.ts +4 -0
- package/dist/runtime/base64.js +77 -0
- package/dist/runtime/crypto.d.ts +7 -0
- package/dist/runtime/crypto.js +421 -0
- package/dist/runtime/fastjson.d.ts +3 -0
- package/dist/runtime/fastjson.js +170 -0
- package/dist/runtime/host.d.ts +73 -0
- package/dist/runtime/host.js +1230 -0
- package/dist/runtime/http.d.ts +40 -0
- package/dist/runtime/http.js +157 -0
- package/dist/runtime/index.d.ts +2 -0
- package/dist/runtime/index.js +1 -0
- package/dist/runtime/intl.d.ts +5 -0
- package/dist/runtime/intl.js +69 -0
- package/dist/runtime/tmpfs.d.ts +129 -0
- package/dist/runtime/tmpfs.js +624 -0
- package/dist/runtime/wasi.d.ts +33 -0
- package/dist/runtime/wasi.js +216 -0
- package/package.json +57 -4
- package/rust/convex_fn/Cargo.toml +32 -0
- package/rust/convex_fn/LICENSE +201 -0
- package/rust/convex_fn/build.rs +3 -0
- package/rust/convex_fn/src/args.rs +416 -0
- package/rust/convex_fn/src/crypto.rs +237 -0
- package/rust/convex_fn/src/ctx.rs +1044 -0
- package/rust/convex_fn/src/dns.rs +356 -0
- package/rust/convex_fn/src/error.rs +179 -0
- package/rust/convex_fn/src/executor.rs +577 -0
- package/rust/convex_fn/src/fanout.rs +106 -0
- package/rust/convex_fn/src/fs.rs +19 -0
- package/rust/convex_fn/src/function_ref.rs +219 -0
- package/rust/convex_fn/src/http.rs +441 -0
- package/rust/convex_fn/src/intl.rs +75 -0
- package/rust/convex_fn/src/lib.rs +242 -0
- package/rust/convex_fn/src/logging.rs +222 -0
- package/rust/convex_fn/src/native.rs +360 -0
- package/rust/convex_fn/src/net.rs +122 -0
- package/rust/convex_fn/src/sys.rs +178 -0
- package/rust/convex_fn/src/values.rs +121 -0
- package/rust/convex_fn/src/wire.rs +579 -0
- package/rust/convex_fn_macros/Cargo.toml +13 -0
- package/rust/convex_fn_macros/LICENSE +201 -0
- package/rust/convex_fn_macros/src/derive.rs +632 -0
- package/rust/convex_fn_macros/src/lib.rs +338 -0
- package/rust/convex_reqwest/Cargo.toml +69 -0
- package/rust/convex_reqwest/LICENSE +201 -0
- package/rust/convex_reqwest/src/blocking.rs +287 -0
- package/rust/convex_reqwest/src/body.rs +152 -0
- package/rust/convex_reqwest/src/client.rs +359 -0
- package/rust/convex_reqwest/src/config_types.rs +150 -0
- package/rust/convex_reqwest/src/error.rs +241 -0
- package/rust/convex_reqwest/src/into_url.rs +57 -0
- package/rust/convex_reqwest/src/lib.rs +42 -0
- package/rust/convex_reqwest/src/multipart.rs +140 -0
- package/rust/convex_reqwest/src/request.rs +358 -0
- package/rust/convex_reqwest/src/response.rs +310 -0
|
@@ -0,0 +1,219 @@
|
|
|
1
|
+
//! Typed function references, as generated into `_generated/api.rs` by
|
|
2
|
+
//! `npx convex dev` (the Rust counterpart of `FunctionReference` in JS).
|
|
3
|
+
|
|
4
|
+
use std::marker::PhantomData;
|
|
5
|
+
|
|
6
|
+
/// A reference to a Convex function taking `A` and returning `R`.
|
|
7
|
+
///
|
|
8
|
+
/// ```ignore
|
|
9
|
+
/// ctx.run_query(api::messages::list, api::messages::ListArgs {}).await?;
|
|
10
|
+
/// ```
|
|
11
|
+
pub struct FunctionRef<A, R> {
|
|
12
|
+
path: &'static str,
|
|
13
|
+
_types: PhantomData<fn(A) -> R>,
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
impl<A, R> FunctionRef<A, R> {
|
|
17
|
+
/// `path` is a Convex function path such as `"messages:list"`.
|
|
18
|
+
pub const fn new(path: &'static str) -> Self {
|
|
19
|
+
FunctionRef {
|
|
20
|
+
path,
|
|
21
|
+
_types: PhantomData,
|
|
22
|
+
}
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
pub const fn path(&self) -> &'static str {
|
|
26
|
+
self.path
|
|
27
|
+
}
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
impl<A, R> Clone for FunctionRef<A, R> {
|
|
31
|
+
fn clone(&self) -> Self {
|
|
32
|
+
*self
|
|
33
|
+
}
|
|
34
|
+
}
|
|
35
|
+
impl<A, R> Copy for FunctionRef<A, R> {}
|
|
36
|
+
|
|
37
|
+
impl<A, R> std::fmt::Debug for FunctionRef<A, R> {
|
|
38
|
+
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
|
39
|
+
write!(f, "FunctionRef({:?})", self.path)
|
|
40
|
+
}
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
/// How a function is addressed on the wire: the key of the syscall args
|
|
44
|
+
/// (`name`, `functionHandle` or `reference`) plus its string value.
|
|
45
|
+
pub trait FunctionAddress {
|
|
46
|
+
/// The path, `function://` handle or component reference path.
|
|
47
|
+
fn function_path(&self) -> &str;
|
|
48
|
+
|
|
49
|
+
/// The syscall args key the path goes under. Plain strings that start with
|
|
50
|
+
/// `function://` are handles, as in JS `getFunctionAddress`.
|
|
51
|
+
fn address_key(&self) -> &'static str {
|
|
52
|
+
"name"
|
|
53
|
+
}
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
fn str_key(s: &str) -> &'static str {
|
|
57
|
+
if s.starts_with(FUNCTION_HANDLE_PREFIX) {
|
|
58
|
+
"functionHandle"
|
|
59
|
+
} else {
|
|
60
|
+
"name"
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
const FUNCTION_HANDLE_PREFIX: &str = "function://";
|
|
65
|
+
|
|
66
|
+
/// A serialized function handle (`function://...`), as returned by
|
|
67
|
+
/// `createFunctionHandle` in JS. Works with `ctx.run_*` and the scheduler, and
|
|
68
|
+
/// is stable across deployments of the same function.
|
|
69
|
+
#[derive(Clone, Debug, PartialEq, Eq, Hash)]
|
|
70
|
+
pub struct FunctionHandle(String);
|
|
71
|
+
|
|
72
|
+
impl FunctionHandle {
|
|
73
|
+
pub fn new(handle: impl Into<String>) -> Self {
|
|
74
|
+
FunctionHandle(handle.into())
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
pub fn as_str(&self) -> &str {
|
|
78
|
+
&self.0
|
|
79
|
+
}
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
/// A reference to a function of a component, by the serialized reference path
|
|
83
|
+
/// that JS `getFunctionAddress` sends as `reference` (the value stored under
|
|
84
|
+
/// `Symbol.for("toReferencePath")` of `components.<name>.<fn>`).
|
|
85
|
+
#[derive(Clone, Debug, PartialEq, Eq, Hash)]
|
|
86
|
+
pub struct ComponentFunctionRef(String);
|
|
87
|
+
|
|
88
|
+
impl ComponentFunctionRef {
|
|
89
|
+
pub fn new(reference_path: impl Into<String>) -> Self {
|
|
90
|
+
ComponentFunctionRef(reference_path.into())
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
pub fn as_str(&self) -> &str {
|
|
94
|
+
&self.0
|
|
95
|
+
}
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
/// Something `ctx.run_query` / `run_mutation` / `run_action` can call: a
|
|
99
|
+
/// string path (untyped, as before), a typed [`FunctionRef`], a
|
|
100
|
+
/// [`FunctionHandle`] or a [`ComponentFunctionRef`].
|
|
101
|
+
pub trait FunctionTarget<A, R>: FunctionAddress {}
|
|
102
|
+
|
|
103
|
+
impl FunctionAddress for &str {
|
|
104
|
+
fn function_path(&self) -> &str {
|
|
105
|
+
self
|
|
106
|
+
}
|
|
107
|
+
fn address_key(&self) -> &'static str {
|
|
108
|
+
str_key(self)
|
|
109
|
+
}
|
|
110
|
+
}
|
|
111
|
+
impl<A, R> FunctionTarget<A, R> for &str {}
|
|
112
|
+
|
|
113
|
+
impl FunctionAddress for String {
|
|
114
|
+
fn function_path(&self) -> &str {
|
|
115
|
+
self
|
|
116
|
+
}
|
|
117
|
+
fn address_key(&self) -> &'static str {
|
|
118
|
+
str_key(self)
|
|
119
|
+
}
|
|
120
|
+
}
|
|
121
|
+
impl<A, R> FunctionTarget<A, R> for String {}
|
|
122
|
+
|
|
123
|
+
impl FunctionAddress for &String {
|
|
124
|
+
fn function_path(&self) -> &str {
|
|
125
|
+
self
|
|
126
|
+
}
|
|
127
|
+
fn address_key(&self) -> &'static str {
|
|
128
|
+
str_key(self)
|
|
129
|
+
}
|
|
130
|
+
}
|
|
131
|
+
impl<A, R> FunctionTarget<A, R> for &String {}
|
|
132
|
+
|
|
133
|
+
impl<A, R> FunctionAddress for FunctionRef<A, R> {
|
|
134
|
+
fn function_path(&self) -> &str {
|
|
135
|
+
self.path
|
|
136
|
+
}
|
|
137
|
+
}
|
|
138
|
+
impl<A, R> FunctionTarget<A, R> for FunctionRef<A, R> {}
|
|
139
|
+
|
|
140
|
+
impl<A, R> FunctionAddress for &FunctionRef<A, R> {
|
|
141
|
+
fn function_path(&self) -> &str {
|
|
142
|
+
self.path
|
|
143
|
+
}
|
|
144
|
+
}
|
|
145
|
+
impl<A, R> FunctionTarget<A, R> for &FunctionRef<A, R> {}
|
|
146
|
+
|
|
147
|
+
impl FunctionAddress for FunctionHandle {
|
|
148
|
+
fn function_path(&self) -> &str {
|
|
149
|
+
&self.0
|
|
150
|
+
}
|
|
151
|
+
fn address_key(&self) -> &'static str {
|
|
152
|
+
"functionHandle"
|
|
153
|
+
}
|
|
154
|
+
}
|
|
155
|
+
impl<A, R> FunctionTarget<A, R> for FunctionHandle {}
|
|
156
|
+
|
|
157
|
+
impl FunctionAddress for &FunctionHandle {
|
|
158
|
+
fn function_path(&self) -> &str {
|
|
159
|
+
&self.0
|
|
160
|
+
}
|
|
161
|
+
fn address_key(&self) -> &'static str {
|
|
162
|
+
"functionHandle"
|
|
163
|
+
}
|
|
164
|
+
}
|
|
165
|
+
impl<A, R> FunctionTarget<A, R> for &FunctionHandle {}
|
|
166
|
+
|
|
167
|
+
impl FunctionAddress for ComponentFunctionRef {
|
|
168
|
+
fn function_path(&self) -> &str {
|
|
169
|
+
&self.0
|
|
170
|
+
}
|
|
171
|
+
fn address_key(&self) -> &'static str {
|
|
172
|
+
"reference"
|
|
173
|
+
}
|
|
174
|
+
}
|
|
175
|
+
impl<A, R> FunctionTarget<A, R> for ComponentFunctionRef {}
|
|
176
|
+
|
|
177
|
+
impl FunctionAddress for &ComponentFunctionRef {
|
|
178
|
+
fn function_path(&self) -> &str {
|
|
179
|
+
&self.0
|
|
180
|
+
}
|
|
181
|
+
fn address_key(&self) -> &'static str {
|
|
182
|
+
"reference"
|
|
183
|
+
}
|
|
184
|
+
}
|
|
185
|
+
impl<A, R> FunctionTarget<A, R> for &ComponentFunctionRef {}
|
|
186
|
+
|
|
187
|
+
/// The `{name | functionHandle | reference: path}` part of a syscall's args.
|
|
188
|
+
pub(crate) fn address_json(target: &impl FunctionAddress) -> serde_json::Value {
|
|
189
|
+
let mut m = serde_json::Map::new();
|
|
190
|
+
m.insert(
|
|
191
|
+
target.address_key().to_string(),
|
|
192
|
+
serde_json::Value::String(target.function_path().to_string()),
|
|
193
|
+
);
|
|
194
|
+
serde_json::Value::Object(m)
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
#[cfg(test)]
|
|
198
|
+
mod tests {
|
|
199
|
+
use super::*;
|
|
200
|
+
|
|
201
|
+
#[test]
|
|
202
|
+
fn addresses() {
|
|
203
|
+
let r: FunctionRef<(), ()> = FunctionRef::new("a:b");
|
|
204
|
+
assert_eq!(address_json(&r), serde_json::json!({"name": "a:b"}));
|
|
205
|
+
assert_eq!(address_json(&"a:b"), serde_json::json!({"name": "a:b"}));
|
|
206
|
+
assert_eq!(
|
|
207
|
+
address_json(&"function://abc"),
|
|
208
|
+
serde_json::json!({"functionHandle": "function://abc"})
|
|
209
|
+
);
|
|
210
|
+
assert_eq!(
|
|
211
|
+
address_json(&FunctionHandle::new("function://x")),
|
|
212
|
+
serde_json::json!({"functionHandle": "function://x"})
|
|
213
|
+
);
|
|
214
|
+
assert_eq!(
|
|
215
|
+
address_json(&ComponentFunctionRef::new("_reference/childComponent/c/f")),
|
|
216
|
+
serde_json::json!({"reference": "_reference/childComponent/c/f"})
|
|
217
|
+
);
|
|
218
|
+
}
|
|
219
|
+
}
|
|
@@ -0,0 +1,441 @@
|
|
|
1
|
+
//! HTTP actions: `#[http_action]` handlers take an [`HttpRequest`] and return an
|
|
2
|
+
//! [`HttpResponse`] (contracts/V8HOST.md "HTTP actions").
|
|
3
|
+
//!
|
|
4
|
+
//! The request head arrives as the call's arguments; its body is a host stream read
|
|
5
|
+
//! chunk by chunk ([`HttpRequest::chunk`]) or whole ([`HttpRequest::bytes`]). A response
|
|
6
|
+
//! is either a buffered body or a streaming one: [`HttpResponse::streaming`] runs a body
|
|
7
|
+
//! producer after the head was sent, writing chunks with [`BodyWriter::write`] while the
|
|
8
|
+
//! client receives them (server-sent events, relayed LLM tokens, ...).
|
|
9
|
+
|
|
10
|
+
use std::{cell::RefCell, future::Future, pin::Pin, rc::Rc};
|
|
11
|
+
|
|
12
|
+
use base64::Engine;
|
|
13
|
+
use serde::{de::DeserializeOwned, Deserialize, Serialize};
|
|
14
|
+
use serde_json::{json, Value};
|
|
15
|
+
|
|
16
|
+
use crate::{
|
|
17
|
+
error::Error,
|
|
18
|
+
executor::{abandon_stream, submit, NextChunk, StreamFuture},
|
|
19
|
+
wire::{self, B64},
|
|
20
|
+
Ctx, Result,
|
|
21
|
+
};
|
|
22
|
+
|
|
23
|
+
/// Convex limits an HTTP action's response body to 20 MiB.
|
|
24
|
+
const MAX_BODY_BYTES: usize = 20 * 1024 * 1024;
|
|
25
|
+
|
|
26
|
+
/// Largest piece sent to the host per `BodyWriter::write` task.
|
|
27
|
+
const WRITE_PIECE_BYTES: usize = 1024 * 1024;
|
|
28
|
+
|
|
29
|
+
#[derive(Deserialize)]
|
|
30
|
+
struct RequestWire {
|
|
31
|
+
method: String,
|
|
32
|
+
url: String,
|
|
33
|
+
#[serde(default)]
|
|
34
|
+
headers: Vec<(String, String)>,
|
|
35
|
+
#[serde(default, rename = "bodyStreamId")]
|
|
36
|
+
body_stream_id: Option<String>,
|
|
37
|
+
/// Whole request body, base64 (hosts without request body streams: the `rustHttpAction` bridge
|
|
38
|
+
/// of the native / wasmtime hosts). Sent together with `buffered`.
|
|
39
|
+
#[serde(default)]
|
|
40
|
+
body: Option<String>,
|
|
41
|
+
/// The host cannot stream a response: a streaming body is collected and returned whole.
|
|
42
|
+
#[serde(default)]
|
|
43
|
+
buffered: bool,
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
/// An incoming HTTP request (the `Request` of a Convex `httpAction`).
|
|
47
|
+
#[derive(Debug, Deserialize)]
|
|
48
|
+
#[serde(from = "RequestWire")]
|
|
49
|
+
pub struct HttpRequest {
|
|
50
|
+
/// HTTP method, upper case (`"GET"`, `"POST"`, ...).
|
|
51
|
+
pub method: String,
|
|
52
|
+
/// The full request URL, e.g. `https://happy-animal-123.convex.site/hook?x=1`.
|
|
53
|
+
pub url: String,
|
|
54
|
+
/// Request headers; names are lower case, as `Headers` yields them.
|
|
55
|
+
pub headers: Vec<(String, String)>,
|
|
56
|
+
stream_id: Option<String>,
|
|
57
|
+
/// The not yet read inline body (`RequestWire::body`).
|
|
58
|
+
inline: Option<Vec<u8>>,
|
|
59
|
+
buffered: bool,
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
impl From<RequestWire> for HttpRequest {
|
|
63
|
+
fn from(w: RequestWire) -> Self {
|
|
64
|
+
// An undecodable inline body reads as an empty one; the bridge always sends valid base64.
|
|
65
|
+
let inline = w.body.map(|b| B64.decode(b).unwrap_or_default());
|
|
66
|
+
HttpRequest {
|
|
67
|
+
method: w.method,
|
|
68
|
+
url: w.url,
|
|
69
|
+
headers: w.headers,
|
|
70
|
+
stream_id: w.body_stream_id,
|
|
71
|
+
inline,
|
|
72
|
+
buffered: w.buffered,
|
|
73
|
+
}
|
|
74
|
+
}
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
impl HttpRequest {
|
|
78
|
+
/// Builds a request value (for tests); `body_stream_id` names a host stream.
|
|
79
|
+
#[doc(hidden)]
|
|
80
|
+
pub fn from_parts(
|
|
81
|
+
method: &str,
|
|
82
|
+
url: &str,
|
|
83
|
+
headers: Vec<(String, String)>,
|
|
84
|
+
body_stream_id: Option<String>,
|
|
85
|
+
) -> Self {
|
|
86
|
+
RequestWire {
|
|
87
|
+
method: method.to_string(),
|
|
88
|
+
url: url.to_string(),
|
|
89
|
+
headers,
|
|
90
|
+
body_stream_id,
|
|
91
|
+
body: None,
|
|
92
|
+
buffered: false,
|
|
93
|
+
}
|
|
94
|
+
.into()
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
/// The first header called `name` (case-insensitive).
|
|
98
|
+
pub fn header(&self, name: &str) -> Option<&str> {
|
|
99
|
+
self.headers
|
|
100
|
+
.iter()
|
|
101
|
+
.find(|(k, _)| k.eq_ignore_ascii_case(name))
|
|
102
|
+
.map(|(_, v)| v.as_str())
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
/// The URL path (without query string and fragment).
|
|
106
|
+
pub fn path(&self) -> &str {
|
|
107
|
+
let rest = self.url.split_once("://").map_or(&*self.url, |(_, r)| r);
|
|
108
|
+
let rest = rest.find('/').map_or("/", |i| &rest[i..]);
|
|
109
|
+
rest.split(['?', '#']).next().unwrap_or("/")
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
/// The raw query string (without the `?`), if any.
|
|
113
|
+
pub fn query(&self) -> Option<&str> {
|
|
114
|
+
let (_, q) = self.url.split_once('?')?;
|
|
115
|
+
Some(q.split('#').next().unwrap_or(q))
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
/// Does the request have a body stream (false for `GET`/`HEAD`)?
|
|
119
|
+
pub fn has_body(&self) -> bool {
|
|
120
|
+
self.stream_id.is_some() || self.inline.is_some()
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
/// The next body chunk, `Ok(None)` once the body ended or if there is none.
|
|
124
|
+
/// After `None` or `Err`, further calls return `Ok(None)`.
|
|
125
|
+
pub async fn chunk(&mut self) -> Result<Option<Vec<u8>>> {
|
|
126
|
+
if let Some(body) = self.inline.take() {
|
|
127
|
+
return Ok((!body.is_empty()).then_some(body));
|
|
128
|
+
}
|
|
129
|
+
let Some(id) = self.stream_id.as_deref() else {
|
|
130
|
+
return Ok(None);
|
|
131
|
+
};
|
|
132
|
+
let r = NextChunk { stream_id: id }.await;
|
|
133
|
+
if !matches!(r, Ok(Some(_))) {
|
|
134
|
+
self.stream_id = None;
|
|
135
|
+
}
|
|
136
|
+
r
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
/// Reads the remaining body into one buffer.
|
|
140
|
+
pub async fn bytes(&mut self) -> Result<Vec<u8>> {
|
|
141
|
+
if let Some(body) = self.inline.take() {
|
|
142
|
+
return Ok(body);
|
|
143
|
+
}
|
|
144
|
+
match self.stream_id.take() {
|
|
145
|
+
Some(stream_id) => StreamFuture::new(stream_id).await,
|
|
146
|
+
None => Ok(vec![]),
|
|
147
|
+
}
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
/// Reads the remaining body as UTF-8 text.
|
|
151
|
+
pub async fn text(&mut self) -> Result<String> {
|
|
152
|
+
Ok(String::from_utf8(self.bytes().await?)?)
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
/// Reads the remaining body and parses it as JSON.
|
|
156
|
+
pub async fn json<T: DeserializeOwned>(&mut self) -> Result<T> {
|
|
157
|
+
Ok(serde_json::from_slice(&self.bytes().await?)?)
|
|
158
|
+
}
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
impl Drop for HttpRequest {
|
|
162
|
+
fn drop(&mut self) {
|
|
163
|
+
if let Some(id) = self.stream_id.take() {
|
|
164
|
+
abandon_stream(&id);
|
|
165
|
+
}
|
|
166
|
+
}
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
type BodyFuture = Pin<Box<dyn Future<Output = Result<()>>>>;
|
|
170
|
+
type BodyFn = Box<dyn FnOnce(BodyWriter) -> BodyFuture>;
|
|
171
|
+
|
|
172
|
+
enum Body {
|
|
173
|
+
Bytes(Vec<u8>),
|
|
174
|
+
Stream(BodyFn),
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
/// The response of an HTTP action: status, headers and a buffered or streaming body.
|
|
178
|
+
pub struct HttpResponse {
|
|
179
|
+
/// Status code, 200..=599.
|
|
180
|
+
pub status: u16,
|
|
181
|
+
/// Response headers.
|
|
182
|
+
pub headers: Vec<(String, String)>,
|
|
183
|
+
body: Body,
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
impl std::fmt::Debug for HttpResponse {
|
|
187
|
+
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
|
188
|
+
let mut d = f.debug_struct("HttpResponse");
|
|
189
|
+
d.field("status", &self.status).field("headers", &self.headers);
|
|
190
|
+
match &self.body {
|
|
191
|
+
Body::Bytes(b) => d.field("body", &format_args!("{} bytes", b.len())),
|
|
192
|
+
Body::Stream(_) => d.field("body", &format_args!("<stream>")),
|
|
193
|
+
};
|
|
194
|
+
d.finish()
|
|
195
|
+
}
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
impl HttpResponse {
|
|
199
|
+
/// An empty response with `status`.
|
|
200
|
+
pub fn new(status: u16) -> Self {
|
|
201
|
+
HttpResponse {
|
|
202
|
+
status,
|
|
203
|
+
headers: vec![],
|
|
204
|
+
body: Body::Bytes(vec![]),
|
|
205
|
+
}
|
|
206
|
+
}
|
|
207
|
+
|
|
208
|
+
/// An empty `200 OK` response.
|
|
209
|
+
pub fn ok() -> Self {
|
|
210
|
+
Self::new(200)
|
|
211
|
+
}
|
|
212
|
+
|
|
213
|
+
/// A `200` response with a `text/plain; charset=utf-8` body.
|
|
214
|
+
pub fn text(body: impl Into<String>) -> Self {
|
|
215
|
+
Self::ok()
|
|
216
|
+
.header("content-type", "text/plain; charset=utf-8")
|
|
217
|
+
.body(body.into().into_bytes())
|
|
218
|
+
}
|
|
219
|
+
|
|
220
|
+
/// A `200` response with `value` as an `application/json` body.
|
|
221
|
+
pub fn json<T: Serialize>(value: &T) -> Result<Self> {
|
|
222
|
+
Ok(Self::ok()
|
|
223
|
+
.header("content-type", "application/json")
|
|
224
|
+
.body(serde_json::to_vec(value)?))
|
|
225
|
+
}
|
|
226
|
+
|
|
227
|
+
/// Sets the status code.
|
|
228
|
+
pub fn status(mut self, status: u16) -> Self {
|
|
229
|
+
self.status = status;
|
|
230
|
+
self
|
|
231
|
+
}
|
|
232
|
+
|
|
233
|
+
/// Adds a header (repeated names are sent as repeated headers).
|
|
234
|
+
pub fn header(mut self, name: impl Into<String>, value: impl Into<String>) -> Self {
|
|
235
|
+
self.headers.push((name.into(), value.into()));
|
|
236
|
+
self
|
|
237
|
+
}
|
|
238
|
+
|
|
239
|
+
/// Sets a buffered body.
|
|
240
|
+
pub fn body(mut self, body: impl Into<Vec<u8>>) -> Self {
|
|
241
|
+
self.body = Body::Bytes(body.into());
|
|
242
|
+
self
|
|
243
|
+
}
|
|
244
|
+
|
|
245
|
+
/// Makes the body a stream: after the head is sent, `produce` runs with a
|
|
246
|
+
/// [`BodyWriter`]; the body ends when the future finishes (an `Err` aborts the stream).
|
|
247
|
+
///
|
|
248
|
+
/// ```ignore
|
|
249
|
+
/// HttpResponse::ok()
|
|
250
|
+
/// .header("content-type", "text/event-stream")
|
|
251
|
+
/// .streaming(|w| async move {
|
|
252
|
+
/// for i in 0..3 { w.write_str(&format!("data: {i}\n\n")).await?; }
|
|
253
|
+
/// Ok(())
|
|
254
|
+
/// })
|
|
255
|
+
/// ```
|
|
256
|
+
pub fn streaming<F, Fut>(mut self, produce: F) -> Self
|
|
257
|
+
where
|
|
258
|
+
F: FnOnce(BodyWriter) -> Fut + 'static,
|
|
259
|
+
Fut: Future<Output = Result<()>> + 'static,
|
|
260
|
+
{
|
|
261
|
+
self.body = Body::Stream(Box::new(move |w| Box::pin(produce(w))));
|
|
262
|
+
self
|
|
263
|
+
}
|
|
264
|
+
|
|
265
|
+
/// The buffered body, `None` for a streaming response.
|
|
266
|
+
pub fn body_bytes(&self) -> Option<&[u8]> {
|
|
267
|
+
match &self.body {
|
|
268
|
+
Body::Bytes(b) => Some(b),
|
|
269
|
+
Body::Stream(_) => None,
|
|
270
|
+
}
|
|
271
|
+
}
|
|
272
|
+
}
|
|
273
|
+
|
|
274
|
+
/// Writes the body of a streaming [`HttpResponse`]. `write` resolves once the host
|
|
275
|
+
/// accepted the bytes (it waits while the client is slower than the producer) and fails
|
|
276
|
+
/// once the client went away. On a host that cannot stream (`buffered`), the writes are
|
|
277
|
+
/// collected and sent as one body when the producer finishes.
|
|
278
|
+
#[derive(Debug)]
|
|
279
|
+
pub struct BodyWriter {
|
|
280
|
+
collected: Option<Rc<RefCell<Vec<u8>>>>,
|
|
281
|
+
}
|
|
282
|
+
|
|
283
|
+
impl BodyWriter {
|
|
284
|
+
/// Sends `data` to the client.
|
|
285
|
+
pub async fn write(&self, data: impl AsRef<[u8]>) -> Result<()> {
|
|
286
|
+
if let Some(buf) = &self.collected {
|
|
287
|
+
let mut buf = buf.borrow_mut();
|
|
288
|
+
if buf.len() + data.as_ref().len() > MAX_BODY_BYTES {
|
|
289
|
+
return Err(Error::msg(format!(
|
|
290
|
+
"HTTP response exceeds the {MAX_BODY_BYTES} byte limit"
|
|
291
|
+
)));
|
|
292
|
+
}
|
|
293
|
+
buf.extend_from_slice(data.as_ref());
|
|
294
|
+
return Ok(());
|
|
295
|
+
}
|
|
296
|
+
for piece in data.as_ref().chunks(WRITE_PIECE_BYTES) {
|
|
297
|
+
submit(&wire::http_write_request(piece)).await?;
|
|
298
|
+
}
|
|
299
|
+
Ok(())
|
|
300
|
+
}
|
|
301
|
+
|
|
302
|
+
/// Sends `text` to the client.
|
|
303
|
+
pub async fn write_str(&self, text: &str) -> Result<()> {
|
|
304
|
+
self.write(text.as_bytes()).await
|
|
305
|
+
}
|
|
306
|
+
}
|
|
307
|
+
|
|
308
|
+
/// Sends the response to the host and returns the `ok` value of the result envelope:
|
|
309
|
+
/// the buffered response (`{status, headers, body}`), or `{"streamed": true}` after a
|
|
310
|
+
/// streaming body was written.
|
|
311
|
+
pub(crate) async fn respond(resp: HttpResponse, buffered: bool) -> Result<Value> {
|
|
312
|
+
if !(200..=599).contains(&resp.status) {
|
|
313
|
+
return Err(Error::msg(format!(
|
|
314
|
+
"invalid HTTP status {} (expected 200..=599)",
|
|
315
|
+
resp.status
|
|
316
|
+
)));
|
|
317
|
+
}
|
|
318
|
+
let envelope = |status: u16, headers: &[(String, String)], b: &[u8]| {
|
|
319
|
+
json!({
|
|
320
|
+
"status": status,
|
|
321
|
+
"headers": headers,
|
|
322
|
+
"body": (!b.is_empty()).then(|| B64.encode(b)),
|
|
323
|
+
})
|
|
324
|
+
};
|
|
325
|
+
match resp.body {
|
|
326
|
+
Body::Bytes(b) => Ok(envelope(resp.status, &resp.headers, &b)),
|
|
327
|
+
Body::Stream(produce) if buffered => {
|
|
328
|
+
let buf = Rc::new(RefCell::new(Vec::new()));
|
|
329
|
+
produce(BodyWriter { collected: Some(buf.clone()) }).await?;
|
|
330
|
+
let body = buf.take();
|
|
331
|
+
Ok(envelope(resp.status, &resp.headers, &body))
|
|
332
|
+
}
|
|
333
|
+
Body::Stream(produce) => {
|
|
334
|
+
submit(&wire::http_start_request(resp.status, &resp.headers)).await?;
|
|
335
|
+
produce(BodyWriter { collected: None }).await?;
|
|
336
|
+
submit(&wire::http_end_request()).await?;
|
|
337
|
+
Ok(json!({ "streamed": true }))
|
|
338
|
+
}
|
|
339
|
+
}
|
|
340
|
+
}
|
|
341
|
+
|
|
342
|
+
/// Adapter used by `#[http_action]`: runs `f` and sends its response.
|
|
343
|
+
#[doc(hidden)]
|
|
344
|
+
pub fn adapter<F, Fut>(
|
|
345
|
+
f: F,
|
|
346
|
+
) -> impl FnOnce(Ctx, HttpRequest) -> Pin<Box<dyn Future<Output = Result<Value>>>>
|
|
347
|
+
where
|
|
348
|
+
F: FnOnce(Ctx, HttpRequest) -> Fut + 'static,
|
|
349
|
+
Fut: Future<Output = Result<HttpResponse>> + 'static,
|
|
350
|
+
{
|
|
351
|
+
move |ctx, req| {
|
|
352
|
+
let buffered = req.buffered;
|
|
353
|
+
Box::pin(async move { respond(f(ctx, req).await?, buffered).await })
|
|
354
|
+
}
|
|
355
|
+
}
|
|
356
|
+
|
|
357
|
+
#[cfg(test)]
|
|
358
|
+
mod tests {
|
|
359
|
+
use super::*;
|
|
360
|
+
|
|
361
|
+
fn req(url: &str) -> HttpRequest {
|
|
362
|
+
HttpRequest::from_parts("GET", url, vec![("Content-Type".into(), "text/x".into())], None)
|
|
363
|
+
}
|
|
364
|
+
|
|
365
|
+
#[test]
|
|
366
|
+
fn path_and_query() {
|
|
367
|
+
assert_eq!(req("https://a.convex.site/hook?x=1#f").path(), "/hook");
|
|
368
|
+
assert_eq!(req("https://a.convex.site/hook?x=1#f").query(), Some("x=1"));
|
|
369
|
+
assert_eq!(req("https://a.convex.site").path(), "/");
|
|
370
|
+
assert_eq!(req("https://a.convex.site/a/b").query(), None);
|
|
371
|
+
}
|
|
372
|
+
|
|
373
|
+
#[test]
|
|
374
|
+
fn header_is_case_insensitive() {
|
|
375
|
+
assert_eq!(req("https://a/").header("content-TYPE"), Some("text/x"));
|
|
376
|
+
assert_eq!(req("https://a/").header("x"), None);
|
|
377
|
+
}
|
|
378
|
+
|
|
379
|
+
#[test]
|
|
380
|
+
fn request_deserializes_from_the_host_head() {
|
|
381
|
+
let r: HttpRequest = serde_json::from_value(json!({
|
|
382
|
+
"method": "POST", "url": "https://a/b", "headers": [["x", "y"]], "bodyStreamId": "s0"
|
|
383
|
+
}))
|
|
384
|
+
.unwrap();
|
|
385
|
+
assert_eq!((r.method.as_str(), r.header("x"), r.has_body()), ("POST", Some("y"), true));
|
|
386
|
+
let r: HttpRequest =
|
|
387
|
+
serde_json::from_value(json!({ "method": "GET", "url": "https://a/", "bodyStreamId": null }))
|
|
388
|
+
.unwrap();
|
|
389
|
+
assert!(!r.has_body() && r.headers.is_empty());
|
|
390
|
+
}
|
|
391
|
+
|
|
392
|
+
#[test]
|
|
393
|
+
fn inline_body_is_read_once_and_marks_a_buffered_host() {
|
|
394
|
+
let head = json!({
|
|
395
|
+
"method": "POST", "url": "https://a/b", "headers": [], "body": B64.encode(b"hello"), "buffered": true
|
|
396
|
+
});
|
|
397
|
+
let mut r: HttpRequest = serde_json::from_value(head.clone()).unwrap();
|
|
398
|
+
assert!(r.has_body() && r.buffered);
|
|
399
|
+
assert_eq!(crate::executor::block_on(r.bytes()).unwrap(), b"hello");
|
|
400
|
+
assert_eq!(crate::executor::block_on(r.bytes()).unwrap(), b"");
|
|
401
|
+
let mut r: HttpRequest = serde_json::from_value(head).unwrap();
|
|
402
|
+
assert_eq!(crate::executor::block_on(r.chunk()).unwrap(), Some(b"hello".to_vec()));
|
|
403
|
+
assert_eq!(crate::executor::block_on(r.chunk()).unwrap(), None);
|
|
404
|
+
let mut r: HttpRequest =
|
|
405
|
+
serde_json::from_value(json!({ "method": "POST", "url": "https://a/", "body": null, "buffered": true })).unwrap();
|
|
406
|
+
assert!(!r.has_body() && crate::executor::block_on(r.text()).unwrap().is_empty());
|
|
407
|
+
}
|
|
408
|
+
|
|
409
|
+
#[test]
|
|
410
|
+
fn buffered_host_collects_a_streaming_body() {
|
|
411
|
+
let resp = HttpResponse::ok().header("content-type", "text/plain").streaming(|w| async move {
|
|
412
|
+
w.write_str("ab").await?;
|
|
413
|
+
w.write(b"cd").await?;
|
|
414
|
+
Ok(())
|
|
415
|
+
});
|
|
416
|
+
let v = crate::executor::block_on(respond(resp, true)).unwrap();
|
|
417
|
+
assert_eq!(v["status"], 200);
|
|
418
|
+
assert_eq!(v["headers"], json!([["content-type", "text/plain"]]));
|
|
419
|
+
assert_eq!(v["body"], B64.encode(b"abcd"));
|
|
420
|
+
// A failing producer fails the call (the buffered host has no truncated bodies).
|
|
421
|
+
let failing = HttpResponse::ok().streaming(|w| async move {
|
|
422
|
+
w.write_str("x").await?;
|
|
423
|
+
Err(Error::msg("boom"))
|
|
424
|
+
});
|
|
425
|
+
assert!(crate::executor::block_on(respond(failing, true)).is_err());
|
|
426
|
+
// An empty collected body is `null`.
|
|
427
|
+
let empty = HttpResponse::ok().streaming(|_| async { Ok(()) });
|
|
428
|
+
assert!(crate::executor::block_on(respond(empty, true)).unwrap()["body"].is_null());
|
|
429
|
+
}
|
|
430
|
+
|
|
431
|
+
#[test]
|
|
432
|
+
fn builders() {
|
|
433
|
+
let r = HttpResponse::text("hi").status(201).header("x-a", "b");
|
|
434
|
+
assert_eq!(r.status, 201);
|
|
435
|
+
assert_eq!(r.body_bytes(), Some(&b"hi"[..]));
|
|
436
|
+
assert_eq!(r.headers[0].0, "content-type");
|
|
437
|
+
let j = HttpResponse::json(&json!({"a": 1})).unwrap();
|
|
438
|
+
assert_eq!(j.body_bytes(), Some(&br#"{"a":1}"#[..]));
|
|
439
|
+
assert!(HttpResponse::ok().streaming(|_| async { Ok(()) }).body_bytes().is_none());
|
|
440
|
+
}
|
|
441
|
+
}
|