@lovrozagar/honey 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/README.md +51 -0
- package/docs/api-ref/go.md +399 -0
- package/docs/api-ref/python.md +399 -0
- package/docs/api-ref/rust.md +399 -0
- package/docs/api-ref/typescript.md +399 -0
- package/docs/invalidate-spec.md +303 -0
- package/docs/sdk.md +722 -0
- package/package.json +120 -0
- package/src/accepts.ts +78 -0
- package/src/body-limit.ts +100 -0
- package/src/build/index.ts +174 -0
- package/src/cli-go/auth.go +15 -0
- package/src/cli-go/config.go +165 -0
- package/src/cli-go/errors.go +57 -0
- package/src/cli-go/multipart.go +55 -0
- package/src/cli-go/output.go +109 -0
- package/src/cli-go/root.go +30 -0
- package/src/cli-go/stream.go +88 -0
- package/src/cli-go/version.go +5 -0
- package/src/cli.ts +204 -0
- package/src/client/error.ts +27 -0
- package/src/client/http.ts +502 -0
- package/src/client/index.ts +127 -0
- package/src/client/sdk.ts +265 -0
- package/src/client/sse.ts +102 -0
- package/src/client/types.ts +211 -0
- package/src/client/ws.ts +115 -0
- package/src/client-go/errors.go +207 -0
- package/src/client-go/invalidation.go +373 -0
- package/src/client-go/realtime.go +317 -0
- package/src/client-go/result.go +20 -0
- package/src/client-go/runtime.go +413 -0
- package/src/client-go/sse.go +149 -0
- package/src/client-go/transport.go +452 -0
- package/src/client-go/ws.go +104 -0
- package/src/client-mcp/README.md +10 -0
- package/src/client-mcp/server.ts +94 -0
- package/src/client-mcp/types.ts +19 -0
- package/src/client-python/_errors.py +115 -0
- package/src/client-python/_invalidation.py +578 -0
- package/src/client-python/_realtime.py +156 -0
- package/src/client-python/_runtime.py +469 -0
- package/src/client-python/_sse.py +99 -0
- package/src/client-python/_transport.py +335 -0
- package/src/client-python/_ws.py +131 -0
- package/src/client-python/py.typed +0 -0
- package/src/client-rust/errors.rs +200 -0
- package/src/client-rust/invalidation.rs +441 -0
- package/src/client-rust/invalidation_sync.rs +306 -0
- package/src/client-rust/realtime.rs +695 -0
- package/src/client-rust/result.rs +22 -0
- package/src/client-rust/runtime.rs +547 -0
- package/src/client-rust/runtime_sync.rs +452 -0
- package/src/client-rust/sse.rs +167 -0
- package/src/client-rust/ws.rs +143 -0
- package/src/cloudflare/realtime.ts +236 -0
- package/src/codegen-go-cli.ts +1180 -0
- package/src/codegen-go.ts +1010 -0
- package/src/codegen-hash.ts +8 -0
- package/src/codegen-ir.ts +517 -0
- package/src/codegen-loaders.ts +53 -0
- package/src/codegen-mcp.ts +364 -0
- package/src/codegen-python.ts +1028 -0
- package/src/codegen-rust.ts +1652 -0
- package/src/codegen-sanitize.ts +67 -0
- package/src/codegen-schema-naming.ts +146 -0
- package/src/codegen.ts +5015 -0
- package/src/context.ts +210 -0
- package/src/cookie-sign.ts +58 -0
- package/src/cookie.ts +89 -0
- package/src/cors.ts +96 -0
- package/src/crypto.ts +19 -0
- package/src/csrf.ts +54 -0
- package/src/curl-logger.ts +235 -0
- package/src/detect-runtime.ts +19 -0
- package/src/error.ts +52 -0
- package/src/errors.ts +87 -0
- package/src/etag.ts +74 -0
- package/src/feature-load.ts +23 -0
- package/src/go-type-emitter.ts +404 -0
- package/src/honey-response.ts +354 -0
- package/src/i18n-slot.ts +20 -0
- package/src/i18n.ts +234 -0
- package/src/index.ts +3843 -0
- package/src/init.ts +149 -0
- package/src/input.ts +8 -0
- package/src/ip-restrict.ts +192 -0
- package/src/logger.ts +149 -0
- package/src/middleware.ts +199 -0
- package/src/node-request.ts +240 -0
- package/src/node.ts +212 -0
- package/src/openapi/register.ts +19 -0
- package/src/openapi/scalar.ts +33 -0
- package/src/openapi/spec-factory.ts +46 -0
- package/src/openapi/spec.ts +31 -0
- package/src/openapi/swagger.ts +34 -0
- package/src/plugin.ts +771 -0
- package/src/powered-by.ts +17 -0
- package/src/pretty-json.ts +39 -0
- package/src/proxy.ts +155 -0
- package/src/python-type-emitter.ts +144 -0
- package/src/realtime/buffer.ts +101 -0
- package/src/realtime/bus.ts +93 -0
- package/src/realtime/longpoll.ts +148 -0
- package/src/realtime/protocol.ts +128 -0
- package/src/realtime/route.ts +104 -0
- package/src/realtime/runtime.ts +430 -0
- package/src/request-id.ts +23 -0
- package/src/request-to-curl.ts +34 -0
- package/src/response.ts +414 -0
- package/src/rust-type-emitter.ts +412 -0
- package/src/secure-headers.ts +69 -0
- package/src/serve-register.ts +8 -0
- package/src/serve-slot.ts +20 -0
- package/src/serve.ts +141 -0
- package/src/server-timing.ts +66 -0
- package/src/static.ts +60 -0
- package/src/telemetry/otel.ts +139 -0
- package/src/testing.ts +114 -0
- package/src/timeout.ts +36 -0
- package/src/tree.ts +464 -0
- package/src/ts-type-emitter.ts +106 -0
- package/src/type-emitter.ts +524 -0
- package/src/type-extractor.ts +749 -0
- package/src/types.ts +638 -0
- package/src/validation.ts +300 -0
- package/src/ws/bun.ts +89 -0
- package/src/ws/cloudflare.ts +133 -0
- package/src/ws/deno.ts +109 -0
- package/src/ws/node.ts +161 -0
- package/src/yaml.ts +80 -0
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
/* Code generated by honey. DO NOT EDIT. */
|
|
2
|
+
|
|
3
|
+
use crate::errors::Error;
|
|
4
|
+
|
|
5
|
+
/// SdkResult is a generic result container for safe-mode SDK calls (throwOnError=false).
|
|
6
|
+
/// In safe mode, methods return SdkResult<T> instead of T.
|
|
7
|
+
/// The error is embedded in the result — the outer Result retains only transport/parse errors.
|
|
8
|
+
/// E defaults to Error (generic transport/api error) but per-op typed error envelopes
|
|
9
|
+
/// can substitute a narrower type (e.g. SdkResult<User, UsersCreateError>).
|
|
10
|
+
pub struct SdkResult<T, E = Error> {
|
|
11
|
+
pub data: Option<T>,
|
|
12
|
+
pub error: Option<E>,
|
|
13
|
+
pub status: u16,
|
|
14
|
+
pub response: crate::runtime::ResponseMeta,
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
impl<T, E> SdkResult<T, E> {
|
|
18
|
+
/// ok returns true when the result carries no error.
|
|
19
|
+
pub fn ok(&self) -> bool {
|
|
20
|
+
self.error.is_none()
|
|
21
|
+
}
|
|
22
|
+
}
|
|
@@ -0,0 +1,547 @@
|
|
|
1
|
+
/* Code generated by honey. DO NOT EDIT. */
|
|
2
|
+
|
|
3
|
+
#![allow(dead_code)]
|
|
4
|
+
|
|
5
|
+
use crate::errors::{raise_for_status, Error};
|
|
6
|
+
use std::collections::HashMap;
|
|
7
|
+
use std::future::Future;
|
|
8
|
+
use std::pin::Pin;
|
|
9
|
+
use std::sync::Arc;
|
|
10
|
+
use std::time::Duration;
|
|
11
|
+
use tokio::sync::Mutex;
|
|
12
|
+
use tokio_util::sync::CancellationToken;
|
|
13
|
+
|
|
14
|
+
/// OnRequestHook is called before each request is sent.
|
|
15
|
+
/// Receives a mutable RequestContext; can mutate headers, url, or body.
|
|
16
|
+
/// Returning Err short-circuits the request.
|
|
17
|
+
pub type OnRequestHook = Arc<
|
|
18
|
+
dyn Fn(
|
|
19
|
+
&mut RequestContext,
|
|
20
|
+
) -> Pin<Box<dyn Future<Output = Result<(), Error>> + Send + '_>>
|
|
21
|
+
+ Send
|
|
22
|
+
+ Sync,
|
|
23
|
+
>;
|
|
24
|
+
|
|
25
|
+
/// OnResponseHook is called after each response is received (before error mapping).
|
|
26
|
+
pub type OnResponseHook = Arc<
|
|
27
|
+
dyn Fn(
|
|
28
|
+
&mut ResponseContext,
|
|
29
|
+
) -> Pin<Box<dyn Future<Output = Result<(), Error>> + Send + '_>>
|
|
30
|
+
+ Send
|
|
31
|
+
+ Sync,
|
|
32
|
+
>;
|
|
33
|
+
|
|
34
|
+
/// RequestContext carries mutable request metadata through OnRequestHook invocations.
|
|
35
|
+
/// `selector` / `is_stale` / `invalidated_by` mirror the TS hook ctx and are
|
|
36
|
+
/// populated from the RequestMeta snapshot taken before the request fires;
|
|
37
|
+
/// zero values when the tracker is disabled.
|
|
38
|
+
pub struct RequestContext {
|
|
39
|
+
pub method: reqwest::Method,
|
|
40
|
+
pub url: String,
|
|
41
|
+
pub path: String,
|
|
42
|
+
pub headers: HashMap<String, String>,
|
|
43
|
+
pub state: Arc<Mutex<HashMap<String, serde_json::Value>>>,
|
|
44
|
+
pub is_retry: bool,
|
|
45
|
+
pub selector: String,
|
|
46
|
+
pub is_stale: bool,
|
|
47
|
+
pub invalidated_by: Vec<String>,
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
/// ResponseContext carries response metadata through OnResponseHook invocations.
|
|
51
|
+
/// `selector` / `is_stale` / `invalidated_by` mirror the RequestContext fields.
|
|
52
|
+
pub struct ResponseContext {
|
|
53
|
+
pub status: u16,
|
|
54
|
+
pub response: Vec<u8>,
|
|
55
|
+
pub headers: reqwest::header::HeaderMap,
|
|
56
|
+
pub state: Arc<Mutex<HashMap<String, serde_json::Value>>>,
|
|
57
|
+
pub is_retry: bool,
|
|
58
|
+
pub selector: String,
|
|
59
|
+
pub is_stale: bool,
|
|
60
|
+
pub invalidated_by: Vec<String>,
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
/// RequestBody represents the different body types a request can carry.
|
|
64
|
+
/// `Stream` carries an owned, single-shot `reqwest::Body` (typically built via
|
|
65
|
+
/// `reqwest::Body::wrap_stream`); like `Multipart`, it cannot be retried on 401
|
|
66
|
+
/// because the underlying body is not `Clone`.
|
|
67
|
+
pub enum RequestBody<'a> {
|
|
68
|
+
None,
|
|
69
|
+
Json(&'a serde_json::Value),
|
|
70
|
+
FormUrl(&'a HashMap<String, String>),
|
|
71
|
+
Multipart(reqwest::multipart::Form),
|
|
72
|
+
Stream(reqwest::Body),
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
/// OnAuthExpiredHook is the async auth-refresh callback type for ClientConfig.
|
|
76
|
+
pub type OnAuthExpiredHook =
|
|
77
|
+
Arc<dyn Fn() -> Pin<Box<dyn Future<Output = Result<String, Error>> + Send>> + Send + Sync>;
|
|
78
|
+
|
|
79
|
+
/// LogEntry is the payload passed to on_log callbacks. Consumers plug in their
|
|
80
|
+
/// own logger; the SDK never formats, levels, or persists. Status is Option so
|
|
81
|
+
/// None encodes "omitted" (request_start) without colliding with a real 0 status.
|
|
82
|
+
/// error is repr-style String on the "error" event; None otherwise. Field layout
|
|
83
|
+
/// matches the TS/Python/Go LogEntry shape for cross-language consumer parity.
|
|
84
|
+
#[derive(Clone, Debug)]
|
|
85
|
+
pub struct LogEntry {
|
|
86
|
+
pub level: String,
|
|
87
|
+
pub event: String,
|
|
88
|
+
pub operation: String,
|
|
89
|
+
pub duration_ms: i64,
|
|
90
|
+
pub status: Option<i32>,
|
|
91
|
+
pub error: Option<String>,
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
/// OnLogHook is the callback type for lifecycle log events. Shared between
|
|
95
|
+
/// ClientConfig (async) and SyncClientConfig (blocking) — both take the same
|
|
96
|
+
/// synchronous Fn signature (emission is fire-and-forget from either runtime).
|
|
97
|
+
pub type OnLogHook = Arc<dyn Fn(LogEntry) + Send + Sync>;
|
|
98
|
+
|
|
99
|
+
/// ClientConfig holds all configuration for the SDK client.
|
|
100
|
+
#[derive(Clone)]
|
|
101
|
+
pub struct ClientConfig {
|
|
102
|
+
pub base_url: String,
|
|
103
|
+
pub bearer_token: Option<String>,
|
|
104
|
+
pub headers: HashMap<String, String>,
|
|
105
|
+
pub timeout: Duration,
|
|
106
|
+
pub throw_on_error: bool,
|
|
107
|
+
pub stale_max_entries: usize,
|
|
108
|
+
pub invalidation: Option<InvalidationConfig>,
|
|
109
|
+
pub on_auth_expired: Option<OnAuthExpiredHook>,
|
|
110
|
+
pub http_client: Option<reqwest::Client>,
|
|
111
|
+
pub auth_header_name: Option<String>,
|
|
112
|
+
pub auth_header_prefix: Option<String>,
|
|
113
|
+
pub on_request: Vec<OnRequestHook>,
|
|
114
|
+
pub on_response: Vec<OnResponseHook>,
|
|
115
|
+
pub on_log: Option<OnLogHook>,
|
|
116
|
+
pub state: Option<Arc<Mutex<HashMap<String, serde_json::Value>>>>,
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
impl Default for ClientConfig {
|
|
120
|
+
fn default() -> Self {
|
|
121
|
+
ClientConfig {
|
|
122
|
+
base_url: String::new(),
|
|
123
|
+
bearer_token: None,
|
|
124
|
+
headers: HashMap::new(),
|
|
125
|
+
timeout: Duration::from_secs(30),
|
|
126
|
+
throw_on_error: true,
|
|
127
|
+
stale_max_entries: 500,
|
|
128
|
+
invalidation: None,
|
|
129
|
+
on_auth_expired: None,
|
|
130
|
+
http_client: None,
|
|
131
|
+
auth_header_name: None,
|
|
132
|
+
auth_header_prefix: None,
|
|
133
|
+
on_request: Vec::new(),
|
|
134
|
+
on_response: Vec::new(),
|
|
135
|
+
on_log: None,
|
|
136
|
+
state: None,
|
|
137
|
+
}
|
|
138
|
+
}
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
/* emit_log invokes the on_log callback if configured, swallowing any panic so a
|
|
142
|
+
faulty logger cannot break the request path. Mirrors Python try/except and Go
|
|
143
|
+
defer recover. */
|
|
144
|
+
fn emit_log(cfg: &ClientConfig, entry: LogEntry) {
|
|
145
|
+
if let Some(ref hook) = cfg.on_log {
|
|
146
|
+
let h = Arc::clone(hook);
|
|
147
|
+
let _ = std::panic::catch_unwind(std::panic::AssertUnwindSafe(move || h(entry)));
|
|
148
|
+
}
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
/* err_status extracts an HTTP status from a typed Error::Api, returning None for
|
|
152
|
+
non-Api errors (Transport/Json/UrlParse/Canceled/Timeout/AuthExpired/Other). */
|
|
153
|
+
fn err_status(err: &Error) -> Option<i32> {
|
|
154
|
+
if let Error::Api(api) = err {
|
|
155
|
+
Some(api.status() as i32)
|
|
156
|
+
} else {
|
|
157
|
+
None
|
|
158
|
+
}
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
/// InvalidationConfig controls cache invalidation behaviour. `stale_time` is in
|
|
162
|
+
/// milliseconds; ≤0 disables the tracker. `stale_max_entries` caps the LRU map
|
|
163
|
+
/// (default 1000 when 0). `max_sources_per_target` caps the `by` list per stale
|
|
164
|
+
/// entry (default 16 when 0).
|
|
165
|
+
#[derive(Clone, Default)]
|
|
166
|
+
pub struct InvalidationConfig {
|
|
167
|
+
pub stale_time: i64,
|
|
168
|
+
pub stale_max_entries: usize,
|
|
169
|
+
pub max_sources_per_target: usize,
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
/// ResponseMeta is the owned response snapshot embedded in SdkResult.response.
|
|
173
|
+
/// Populated from a completed RequestResult; all fields are Clone-able.
|
|
174
|
+
pub struct ResponseMeta {
|
|
175
|
+
pub status: u16,
|
|
176
|
+
pub headers: reqwest::header::HeaderMap,
|
|
177
|
+
pub url: String,
|
|
178
|
+
pub body: Vec<u8>,
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
/// RequestResult carries the raw bytes, status, headers, and final URL from a completed request.
|
|
182
|
+
#[allow(dead_code)]
|
|
183
|
+
pub(crate) struct RequestResult {
|
|
184
|
+
pub body: Vec<u8>,
|
|
185
|
+
pub status: u16,
|
|
186
|
+
pub headers: reqwest::header::HeaderMap,
|
|
187
|
+
pub url: String,
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
/// do_request executes an HTTP request, injects auth, merges headers, handles
|
|
191
|
+
/// on_auth_expired retry on 401, fires on_request/on_response hooks, and wraps
|
|
192
|
+
/// errors into typed Error variants. Emits on_log lifecycle entries (request_start
|
|
193
|
+
/// once at entry, response_received once for the final response, error on terminal
|
|
194
|
+
/// failure) — matching TS/Python/Go behavior where retry rounds do not double-emit.
|
|
195
|
+
#[allow(clippy::too_many_arguments)]
|
|
196
|
+
pub(crate) async fn do_request(
|
|
197
|
+
client: &reqwest::Client,
|
|
198
|
+
cfg: &ClientConfig,
|
|
199
|
+
method: reqwest::Method,
|
|
200
|
+
raw_url: &str,
|
|
201
|
+
query: &[(String, String)],
|
|
202
|
+
body: RequestBody<'_>,
|
|
203
|
+
call_headers: Option<&HashMap<String, String>>,
|
|
204
|
+
timeout: Option<std::time::Duration>,
|
|
205
|
+
cancel_token: Option<CancellationToken>,
|
|
206
|
+
operation: &str,
|
|
207
|
+
request_meta: Option<&crate::invalidation::RequestMeta>,
|
|
208
|
+
) -> Result<RequestResult, Error> {
|
|
209
|
+
let start = std::time::Instant::now();
|
|
210
|
+
|
|
211
|
+
/* Pre-flight cancellation check — short-circuit before any work. */
|
|
212
|
+
if let Some(ref tok) = cancel_token {
|
|
213
|
+
if tok.is_cancelled() {
|
|
214
|
+
emit_log(
|
|
215
|
+
cfg,
|
|
216
|
+
LogEntry {
|
|
217
|
+
level: "error".to_string(),
|
|
218
|
+
event: "error".to_string(),
|
|
219
|
+
operation: operation.to_string(),
|
|
220
|
+
duration_ms: start.elapsed().as_millis() as i64,
|
|
221
|
+
status: None,
|
|
222
|
+
error: Some("Canceled".to_string()),
|
|
223
|
+
},
|
|
224
|
+
);
|
|
225
|
+
return Err(Error::Canceled);
|
|
226
|
+
}
|
|
227
|
+
}
|
|
228
|
+
|
|
229
|
+
emit_log(
|
|
230
|
+
cfg,
|
|
231
|
+
LogEntry {
|
|
232
|
+
level: "debug".to_string(),
|
|
233
|
+
event: "request_start".to_string(),
|
|
234
|
+
operation: operation.to_string(),
|
|
235
|
+
duration_ms: 0,
|
|
236
|
+
status: None,
|
|
237
|
+
error: None,
|
|
238
|
+
},
|
|
239
|
+
);
|
|
240
|
+
|
|
241
|
+
/* Reborrow body for retry path (Json/FormUrl hold references so reborrow is valid;
|
|
242
|
+
Multipart + Stream own non-Clone values and cannot be retried — 401 falls through) */
|
|
243
|
+
let retry_body: Option<RequestBody<'_>> = match &body {
|
|
244
|
+
RequestBody::None => Some(RequestBody::None),
|
|
245
|
+
RequestBody::Json(v) => Some(RequestBody::Json(v)),
|
|
246
|
+
RequestBody::FormUrl(m) => Some(RequestBody::FormUrl(m)),
|
|
247
|
+
RequestBody::Multipart(_) => None,
|
|
248
|
+
RequestBody::Stream(_) => None,
|
|
249
|
+
};
|
|
250
|
+
|
|
251
|
+
let mut result = match execute_request(
|
|
252
|
+
client,
|
|
253
|
+
cfg,
|
|
254
|
+
method.clone(),
|
|
255
|
+
raw_url,
|
|
256
|
+
query,
|
|
257
|
+
body,
|
|
258
|
+
call_headers,
|
|
259
|
+
cfg.bearer_token.as_deref(),
|
|
260
|
+
false,
|
|
261
|
+
timeout,
|
|
262
|
+
cancel_token.clone(),
|
|
263
|
+
request_meta,
|
|
264
|
+
)
|
|
265
|
+
.await
|
|
266
|
+
{
|
|
267
|
+
Ok(r) => r,
|
|
268
|
+
Err(e) => {
|
|
269
|
+
emit_log(
|
|
270
|
+
cfg,
|
|
271
|
+
LogEntry {
|
|
272
|
+
level: "error".to_string(),
|
|
273
|
+
event: "error".to_string(),
|
|
274
|
+
operation: operation.to_string(),
|
|
275
|
+
duration_ms: start.elapsed().as_millis() as i64,
|
|
276
|
+
status: err_status(&e),
|
|
277
|
+
error: Some(format!("{:?}", e)),
|
|
278
|
+
},
|
|
279
|
+
);
|
|
280
|
+
return Err(e);
|
|
281
|
+
}
|
|
282
|
+
};
|
|
283
|
+
|
|
284
|
+
if result.status == 401 {
|
|
285
|
+
if let Some(ref hook) = cfg.on_auth_expired {
|
|
286
|
+
if let Some(retry) = retry_body {
|
|
287
|
+
let new_token = match hook().await {
|
|
288
|
+
Ok(t) => t,
|
|
289
|
+
Err(e) => {
|
|
290
|
+
let mapped = Error::AuthExpired(e.to_string());
|
|
291
|
+
emit_log(
|
|
292
|
+
cfg,
|
|
293
|
+
LogEntry {
|
|
294
|
+
level: "error".to_string(),
|
|
295
|
+
event: "error".to_string(),
|
|
296
|
+
operation: operation.to_string(),
|
|
297
|
+
duration_ms: start.elapsed().as_millis() as i64,
|
|
298
|
+
status: err_status(&mapped),
|
|
299
|
+
error: Some(format!("{:?}", mapped)),
|
|
300
|
+
},
|
|
301
|
+
);
|
|
302
|
+
return Err(mapped);
|
|
303
|
+
}
|
|
304
|
+
};
|
|
305
|
+
result = match execute_request(
|
|
306
|
+
client,
|
|
307
|
+
cfg,
|
|
308
|
+
method,
|
|
309
|
+
raw_url,
|
|
310
|
+
query,
|
|
311
|
+
retry,
|
|
312
|
+
call_headers,
|
|
313
|
+
Some(&new_token),
|
|
314
|
+
true,
|
|
315
|
+
timeout,
|
|
316
|
+
cancel_token,
|
|
317
|
+
request_meta,
|
|
318
|
+
)
|
|
319
|
+
.await
|
|
320
|
+
{
|
|
321
|
+
Ok(r) => r,
|
|
322
|
+
Err(e) => {
|
|
323
|
+
emit_log(
|
|
324
|
+
cfg,
|
|
325
|
+
LogEntry {
|
|
326
|
+
level: "error".to_string(),
|
|
327
|
+
event: "error".to_string(),
|
|
328
|
+
operation: operation.to_string(),
|
|
329
|
+
duration_ms: start.elapsed().as_millis() as i64,
|
|
330
|
+
status: err_status(&e),
|
|
331
|
+
error: Some(format!("{:?}", e)),
|
|
332
|
+
},
|
|
333
|
+
);
|
|
334
|
+
return Err(e);
|
|
335
|
+
}
|
|
336
|
+
};
|
|
337
|
+
}
|
|
338
|
+
/* Multipart/Stream 401: Form/Body is not Clone, cannot retry; fall through to error mapping */
|
|
339
|
+
}
|
|
340
|
+
}
|
|
341
|
+
|
|
342
|
+
let status_i32 = result.status as i32;
|
|
343
|
+
let level = if result.status >= 400 { "warn" } else { "info" };
|
|
344
|
+
emit_log(
|
|
345
|
+
cfg,
|
|
346
|
+
LogEntry {
|
|
347
|
+
level: level.to_string(),
|
|
348
|
+
event: "response_received".to_string(),
|
|
349
|
+
operation: operation.to_string(),
|
|
350
|
+
duration_ms: start.elapsed().as_millis() as i64,
|
|
351
|
+
status: Some(status_i32),
|
|
352
|
+
error: None,
|
|
353
|
+
},
|
|
354
|
+
);
|
|
355
|
+
|
|
356
|
+
if let Some(err) = raise_for_status(result.status, result.body.clone()) {
|
|
357
|
+
emit_log(
|
|
358
|
+
cfg,
|
|
359
|
+
LogEntry {
|
|
360
|
+
level: "error".to_string(),
|
|
361
|
+
event: "error".to_string(),
|
|
362
|
+
operation: operation.to_string(),
|
|
363
|
+
duration_ms: start.elapsed().as_millis() as i64,
|
|
364
|
+
status: Some(status_i32),
|
|
365
|
+
error: Some(format!("{:?}", err)),
|
|
366
|
+
},
|
|
367
|
+
);
|
|
368
|
+
return Err(err);
|
|
369
|
+
}
|
|
370
|
+
|
|
371
|
+
Ok(result)
|
|
372
|
+
}
|
|
373
|
+
|
|
374
|
+
#[allow(clippy::too_many_arguments)]
|
|
375
|
+
async fn execute_request(
|
|
376
|
+
client: &reqwest::Client,
|
|
377
|
+
cfg: &ClientConfig,
|
|
378
|
+
method: reqwest::Method,
|
|
379
|
+
raw_url: &str,
|
|
380
|
+
query: &[(String, String)],
|
|
381
|
+
body: RequestBody<'_>,
|
|
382
|
+
call_headers: Option<&HashMap<String, String>>,
|
|
383
|
+
bearer_token: Option<&str>,
|
|
384
|
+
is_retry: bool,
|
|
385
|
+
per_call_timeout: Option<std::time::Duration>,
|
|
386
|
+
cancel_token: Option<CancellationToken>,
|
|
387
|
+
request_meta: Option<&crate::invalidation::RequestMeta>,
|
|
388
|
+
) -> Result<RequestResult, Error> {
|
|
389
|
+
let url = build_url(&cfg.base_url, raw_url, query)?;
|
|
390
|
+
let url_str = url.to_string();
|
|
391
|
+
|
|
392
|
+
/* build merged headers map for hook inspection */
|
|
393
|
+
let mut hook_headers: HashMap<String, String> = cfg.headers.clone();
|
|
394
|
+
if let Some(ch) = call_headers {
|
|
395
|
+
for (k, v) in ch {
|
|
396
|
+
hook_headers.insert(k.clone(), v.clone());
|
|
397
|
+
}
|
|
398
|
+
}
|
|
399
|
+
if let Some(token) = bearer_token {
|
|
400
|
+
if !token.is_empty() {
|
|
401
|
+
let name = cfg.auth_header_name.as_deref().unwrap_or("Authorization");
|
|
402
|
+
let prefix = cfg.auth_header_prefix.as_deref().unwrap_or("Bearer ");
|
|
403
|
+
hook_headers.insert(name.to_string(), format!("{}{}", prefix, token));
|
|
404
|
+
}
|
|
405
|
+
}
|
|
406
|
+
|
|
407
|
+
let state = cfg
|
|
408
|
+
.state
|
|
409
|
+
.clone()
|
|
410
|
+
.unwrap_or_else(|| Arc::new(Mutex::new(HashMap::new())));
|
|
411
|
+
|
|
412
|
+
let (meta_selector, meta_is_stale, meta_by) = match request_meta {
|
|
413
|
+
Some(m) => (m.selector.clone(), m.is_stale, m.invalidated_by.clone()),
|
|
414
|
+
None => (String::new(), false, Vec::new()),
|
|
415
|
+
};
|
|
416
|
+
|
|
417
|
+
let mut req_ctx = RequestContext {
|
|
418
|
+
method: method.clone(),
|
|
419
|
+
url: url_str.clone(),
|
|
420
|
+
path: raw_url.to_string(),
|
|
421
|
+
headers: hook_headers,
|
|
422
|
+
state: Arc::clone(&state),
|
|
423
|
+
is_retry,
|
|
424
|
+
selector: meta_selector.clone(),
|
|
425
|
+
is_stale: meta_is_stale,
|
|
426
|
+
invalidated_by: meta_by.clone(),
|
|
427
|
+
};
|
|
428
|
+
|
|
429
|
+
/* fire on_request hooks */
|
|
430
|
+
for hook in cfg.on_request.iter() {
|
|
431
|
+
hook(&mut req_ctx).await?;
|
|
432
|
+
}
|
|
433
|
+
|
|
434
|
+
let mut req = client.request(req_ctx.method.clone(), &req_ctx.url);
|
|
435
|
+
|
|
436
|
+
/* apply headers from context (already merged above + any hook mutations) */
|
|
437
|
+
for (k, v) in &req_ctx.headers {
|
|
438
|
+
req = req.header(k.as_str(), v.as_str());
|
|
439
|
+
}
|
|
440
|
+
|
|
441
|
+
/* apply body and content-type */
|
|
442
|
+
req = match body {
|
|
443
|
+
RequestBody::Json(v) => {
|
|
444
|
+
req = req.header("Content-Type", "application/json");
|
|
445
|
+
req.json(v)
|
|
446
|
+
}
|
|
447
|
+
RequestBody::FormUrl(m) => req.form(m),
|
|
448
|
+
RequestBody::Multipart(f) => req.multipart(f),
|
|
449
|
+
RequestBody::Stream(b) => {
|
|
450
|
+
req = req.header("Content-Type", "application/octet-stream");
|
|
451
|
+
req.body(b)
|
|
452
|
+
}
|
|
453
|
+
RequestBody::None => req,
|
|
454
|
+
};
|
|
455
|
+
|
|
456
|
+
/* apply per-call timeout (overrides cfg.timeout) */
|
|
457
|
+
let timeout = per_call_timeout;
|
|
458
|
+
let effective_timeout = timeout.unwrap_or(cfg.timeout);
|
|
459
|
+
req = req.timeout(effective_timeout);
|
|
460
|
+
|
|
461
|
+
/* Race the send future against cancel_token.cancelled() so external cancellation
|
|
462
|
+
takes priority over an in-flight HTTP send. */
|
|
463
|
+
let resp = match cancel_token.clone() {
|
|
464
|
+
Some(tok) => {
|
|
465
|
+
tokio::select! {
|
|
466
|
+
biased;
|
|
467
|
+
_ = tok.cancelled() => return Err(Error::Canceled),
|
|
468
|
+
r = req.send() => r,
|
|
469
|
+
}
|
|
470
|
+
}
|
|
471
|
+
None => req.send().await,
|
|
472
|
+
}
|
|
473
|
+
.map_err(|e| {
|
|
474
|
+
if e.is_timeout() {
|
|
475
|
+
Error::Timeout
|
|
476
|
+
} else if e.is_request() {
|
|
477
|
+
Error::Canceled
|
|
478
|
+
} else {
|
|
479
|
+
Error::Transport(e)
|
|
480
|
+
}
|
|
481
|
+
})?;
|
|
482
|
+
|
|
483
|
+
let status = resp.status().as_u16();
|
|
484
|
+
let resp_headers = resp.headers().clone();
|
|
485
|
+
let resp_url = resp.url().to_string();
|
|
486
|
+
let bytes = match cancel_token {
|
|
487
|
+
Some(tok) => {
|
|
488
|
+
tokio::select! {
|
|
489
|
+
biased;
|
|
490
|
+
_ = tok.cancelled() => return Err(Error::Canceled),
|
|
491
|
+
r = resp.bytes() => r,
|
|
492
|
+
}
|
|
493
|
+
}
|
|
494
|
+
None => resp.bytes().await,
|
|
495
|
+
}
|
|
496
|
+
.map_err(|e| {
|
|
497
|
+
if e.is_timeout() {
|
|
498
|
+
Error::Timeout
|
|
499
|
+
} else {
|
|
500
|
+
Error::Transport(e)
|
|
501
|
+
}
|
|
502
|
+
})?;
|
|
503
|
+
let body_bytes = bytes.to_vec();
|
|
504
|
+
|
|
505
|
+
let mut resp_ctx = ResponseContext {
|
|
506
|
+
status,
|
|
507
|
+
response: body_bytes.clone(),
|
|
508
|
+
headers: resp_headers.clone(),
|
|
509
|
+
state: Arc::clone(&state),
|
|
510
|
+
is_retry,
|
|
511
|
+
selector: meta_selector,
|
|
512
|
+
is_stale: meta_is_stale,
|
|
513
|
+
invalidated_by: meta_by,
|
|
514
|
+
};
|
|
515
|
+
|
|
516
|
+
/* fire on_response hooks */
|
|
517
|
+
for hook in cfg.on_response.iter() {
|
|
518
|
+
hook(&mut resp_ctx).await?;
|
|
519
|
+
}
|
|
520
|
+
|
|
521
|
+
Ok(RequestResult {
|
|
522
|
+
body: body_bytes,
|
|
523
|
+
status,
|
|
524
|
+
headers: resp_headers,
|
|
525
|
+
url: resp_url,
|
|
526
|
+
})
|
|
527
|
+
}
|
|
528
|
+
|
|
529
|
+
/// build_url constructs the full URL from base + path + query params.
|
|
530
|
+
#[allow(dead_code)]
|
|
531
|
+
pub(crate) fn build_url(
|
|
532
|
+
base_url: &str,
|
|
533
|
+
path: &str,
|
|
534
|
+
query: &[(String, String)],
|
|
535
|
+
) -> Result<url::Url, Error> {
|
|
536
|
+
let base = url::Url::parse(base_url)?;
|
|
537
|
+
let joined = base.join(path)?;
|
|
538
|
+
let mut url = joined;
|
|
539
|
+
if !query.is_empty() {
|
|
540
|
+
let mut pairs = url.query_pairs_mut();
|
|
541
|
+
for (k, v) in query {
|
|
542
|
+
pairs.append_pair(k, v);
|
|
543
|
+
}
|
|
544
|
+
}
|
|
545
|
+
Ok(url)
|
|
546
|
+
}
|
|
547
|
+
|