1use crate::body_evidence::capture_json_body;
2use crate::invocation::{self, InvocationContext};
3use crate::protocol::{
4 ProviderHttpProxyInvokeRequest, ProviderHttpProxyInvokeResponse, ProviderInvocationMode,
5 ProviderOperationKind,
6};
7use crate::request::{ProxyRequestBody, apply_grpc_proxy_request_policy};
8use crate::{ProviderHttpProxyMatch, ProviderHttpProxyRegistry};
9use axum::Json;
10use axum::body::{Body, Bytes, to_bytes};
11use axum::extract::{Path, State};
12use axum::http::{HeaderMap, Request};
13use platform_core::{
14 AppContext, AppError, ErrorCode, ProviderHttpCallBodyEvidence, ProviderHttpCallRecord,
15 insert_provider_http_call_with_body_evidence,
16};
17use platform_http::{
18 AdminActor, ApiErrorResponse, ApiOpenApiRouter, ErrorResponse, HttpRequestContext,
19 OpenApiRouter, routes,
20};
21use platform_module::ModuleHttpMethod;
22use serde::{Serialize, Serializer};
23use serde_json::{Value, json};
24use std::collections::BTreeMap;
25use std::sync::{Arc, OnceLock, RwLock};
26use std::time::{Duration, Instant};
27use utoipa::ToSchema;
28
29static PROVIDER_HTTP_PROXY_REGISTRY: OnceLock<RwLock<Arc<ProviderHttpProxyRegistry>>> =
30 OnceLock::new();
31const MAX_PROXY_DELETE_REQUEST_BYTES: usize = 1024 * 1024;
32
33#[derive(Debug, Serialize, ToSchema)]
34pub struct ProviderHttpProxyResponse {
35 pub status: ProviderHttpProxyStatus,
36 pub module_name: String,
37 pub method: ModuleHttpMethod,
38 pub declared_path: String,
39 pub provider_path: String,
40 pub capability: String,
41 pub path_params: BTreeMap<String, String>,
42 pub data: Value,
43}
44
45#[derive(Debug, ToSchema)]
46#[serde(rename_all = "snake_case")]
47pub enum ProviderHttpProxyStatus {
48 Forwarded,
49}
50
51impl Serialize for ProviderHttpProxyStatus {
52 fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
53 where
54 S: Serializer,
55 {
56 match self {
57 Self::Forwarded => serializer.serialize_str("forwarded"),
58 }
59 }
60}
61
62#[must_use]
63pub fn router() -> ApiOpenApiRouter {
64 OpenApiRouter::new()
65 .routes(routes!(proxy_get))
66 .routes(routes!(proxy_post))
67 .routes(routes!(proxy_put))
68 .routes(routes!(proxy_patch))
69 .routes(routes!(proxy_delete))
70}
71
72pub fn install_provider_http_proxy_registry(registry: ProviderHttpProxyRegistry) {
73 let storage = PROVIDER_HTTP_PROXY_REGISTRY
74 .get_or_init(|| RwLock::new(Arc::new(ProviderHttpProxyRegistry::from_modules(&[], &[]))));
75 *storage
76 .write()
77 .expect("provider HTTP proxy registry lock poisoned") = Arc::new(registry);
78}
79
80fn provider_http_proxy_registry() -> Arc<ProviderHttpProxyRegistry> {
81 PROVIDER_HTTP_PROXY_REGISTRY
82 .get()
83 .map(|storage| {
84 storage
85 .read()
86 .expect("provider HTTP proxy registry lock poisoned")
87 .clone()
88 })
89 .unwrap_or_else(|| Arc::new(ProviderHttpProxyRegistry::from_modules(&[], &[])))
90}
91
92#[utoipa::path(
93 get,
94 path = "/modules/{module}/http/{*path}",
95 operation_id = "service_module_http_proxy_get",
96 tag = "modules",
97 params(
98 ("module" = String, Path, description = "Configured Service-provided Module name"),
99 ("path" = String, Path, description = "Module-local HTTP path matched against the Service-provided manifest"),
100 ("authorization" = String, Header, description = "Development service bearer token")
101 ),
102 responses(
103 (status = 200, description = "Provider route forwarded through the host.", body = ProviderHttpProxyResponse, content_type = "application/json"),
104 (status = 401, description = "Authentication is required", body = ErrorResponse, content_type = "application/problem+json"),
105 (status = 403, description = "Service/system authentication or declared capability is required", body = ErrorResponse, content_type = "application/problem+json"),
106 (status = 404, description = "No configured Service-provided route matched", body = ErrorResponse, content_type = "application/problem+json"),
107 (status = 502, description = "Provider export request failed", body = ErrorResponse, content_type = "application/problem+json"),
108 )
109)]
110async fn proxy_get(
111 State(ctx): State<AppContext>,
112 admin: AdminActor,
113 HttpRequestContext(request_ctx): HttpRequestContext,
114 headers: HeaderMap,
115 Path((module, path)): Path<(String, String)>,
116) -> Result<Json<ProviderHttpProxyResponse>, ApiErrorResponse> {
117 let request_path = format!("/{path}");
118 let matched = provider_http_proxy_registry()
119 .match_route(&module, ModuleHttpMethod::Get, &request_path)
120 .ok_or_else(|| {
121 ApiErrorResponse::with_context(
122 AppError::new(
123 ErrorCode::NotFound,
124 format!("provider HTTP route not found: {module}{request_path}"),
125 ),
126 &request_ctx,
127 )
128 })?;
129
130 ensure_capability(&admin, &matched, &request_ctx)?;
131 let data = forward_get(&ctx, &matched, &headers, &request_ctx).await?;
132 Ok(Json(ProviderHttpProxyResponse::from_match(matched, data)))
133}
134
135#[utoipa::path(
136 post,
137 path = "/modules/{module}/http/{*path}",
138 operation_id = "service_module_http_proxy_post",
139 tag = "modules",
140 request_body(
141 content = Value,
142 content_type = "application/json",
143 description = "JSON request body forwarded to the matched Service-provided Module route"
144 ),
145 params(
146 ("module" = String, Path, description = "Configured Service-provided Module name"),
147 ("path" = String, Path, description = "Module-local HTTP path matched against the Service-provided manifest"),
148 ("authorization" = String, Header, description = "Development service bearer token")
149 ),
150 responses(
151 (status = 200, description = "Provider route forwarded through the host.", body = ProviderHttpProxyResponse, content_type = "application/json"),
152 (status = 400, description = "Request body policy rejected the request", body = ErrorResponse, content_type = "application/problem+json"),
153 (status = 401, description = "Authentication is required", body = ErrorResponse, content_type = "application/problem+json"),
154 (status = 403, description = "Service/system authentication or declared capability is required", body = ErrorResponse, content_type = "application/problem+json"),
155 (status = 404, description = "No configured Service-provided route matched", body = ErrorResponse, content_type = "application/problem+json"),
156 (status = 502, description = "Provider export request failed", body = ErrorResponse, content_type = "application/problem+json"),
157 )
158)]
159async fn proxy_post(
160 State(ctx): State<AppContext>,
161 admin: AdminActor,
162 HttpRequestContext(request_ctx): HttpRequestContext,
163 headers: HeaderMap,
164 Path((module, path)): Path<(String, String)>,
165 body: Bytes,
166) -> Result<Json<ProviderHttpProxyResponse>, ApiErrorResponse> {
167 proxy_body_method(
168 ModuleHttpMethod::Post,
169 ctx,
170 admin,
171 request_ctx,
172 headers,
173 module,
174 path,
175 body,
176 )
177 .await
178}
179
180#[utoipa::path(
181 put,
182 path = "/modules/{module}/http/{*path}",
183 operation_id = "service_module_http_proxy_put",
184 tag = "modules",
185 request_body(
186 content = Value,
187 content_type = "application/json",
188 description = "JSON request body forwarded to the matched Service-provided Module route"
189 ),
190 params(
191 ("module" = String, Path, description = "Configured Service-provided Module name"),
192 ("path" = String, Path, description = "Module-local HTTP path matched against the Service-provided manifest"),
193 ("authorization" = String, Header, description = "Development service bearer token")
194 ),
195 responses(
196 (status = 200, description = "Provider route forwarded through the host.", body = ProviderHttpProxyResponse, content_type = "application/json"),
197 (status = 400, description = "Request body policy rejected the request", body = ErrorResponse, content_type = "application/problem+json"),
198 (status = 401, description = "Authentication is required", body = ErrorResponse, content_type = "application/problem+json"),
199 (status = 403, description = "Service/system authentication or declared capability is required", body = ErrorResponse, content_type = "application/problem+json"),
200 (status = 404, description = "No configured Service-provided route matched", body = ErrorResponse, content_type = "application/problem+json"),
201 (status = 502, description = "Provider export request failed", body = ErrorResponse, content_type = "application/problem+json"),
202 )
203)]
204async fn proxy_put(
205 State(ctx): State<AppContext>,
206 admin: AdminActor,
207 HttpRequestContext(request_ctx): HttpRequestContext,
208 headers: HeaderMap,
209 Path((module, path)): Path<(String, String)>,
210 body: Bytes,
211) -> Result<Json<ProviderHttpProxyResponse>, ApiErrorResponse> {
212 proxy_body_method(
213 ModuleHttpMethod::Put,
214 ctx,
215 admin,
216 request_ctx,
217 headers,
218 module,
219 path,
220 body,
221 )
222 .await
223}
224
225#[utoipa::path(
226 patch,
227 path = "/modules/{module}/http/{*path}",
228 operation_id = "service_module_http_proxy_patch",
229 tag = "modules",
230 request_body(
231 content = Value,
232 content_type = "application/json",
233 description = "JSON request body forwarded to the matched Service-provided Module route"
234 ),
235 params(
236 ("module" = String, Path, description = "Configured Service-provided Module name"),
237 ("path" = String, Path, description = "Module-local HTTP path matched against the Service-provided manifest"),
238 ("authorization" = String, Header, description = "Development service bearer token")
239 ),
240 responses(
241 (status = 200, description = "Provider route forwarded through the host.", body = ProviderHttpProxyResponse, content_type = "application/json"),
242 (status = 400, description = "Request body policy rejected the request", body = ErrorResponse, content_type = "application/problem+json"),
243 (status = 401, description = "Authentication is required", body = ErrorResponse, content_type = "application/problem+json"),
244 (status = 403, description = "Service/system authentication or declared capability is required", body = ErrorResponse, content_type = "application/problem+json"),
245 (status = 404, description = "No configured Service-provided route matched", body = ErrorResponse, content_type = "application/problem+json"),
246 (status = 502, description = "Provider export request failed", body = ErrorResponse, content_type = "application/problem+json"),
247 )
248)]
249async fn proxy_patch(
250 State(ctx): State<AppContext>,
251 admin: AdminActor,
252 HttpRequestContext(request_ctx): HttpRequestContext,
253 headers: HeaderMap,
254 Path((module, path)): Path<(String, String)>,
255 body: Bytes,
256) -> Result<Json<ProviderHttpProxyResponse>, ApiErrorResponse> {
257 proxy_body_method(
258 ModuleHttpMethod::Patch,
259 ctx,
260 admin,
261 request_ctx,
262 headers,
263 module,
264 path,
265 body,
266 )
267 .await
268}
269
270#[utoipa::path(
271 delete,
272 path = "/modules/{module}/http/{*path}",
273 operation_id = "service_module_http_proxy_delete",
274 tag = "modules",
275 params(
276 ("module" = String, Path, description = "Configured Service-provided Module name"),
277 ("path" = String, Path, description = "Module-local HTTP path matched against the Service-provided manifest"),
278 ("authorization" = String, Header, description = "Development service bearer token")
279 ),
280 responses(
281 (status = 200, description = "Provider route forwarded through the host.", body = ProviderHttpProxyResponse, content_type = "application/json"),
282 (status = 400, description = "Request body policy rejected the request", body = ErrorResponse, content_type = "application/problem+json"),
283 (status = 401, description = "Authentication is required", body = ErrorResponse, content_type = "application/problem+json"),
284 (status = 403, description = "Service/system authentication or declared capability is required", body = ErrorResponse, content_type = "application/problem+json"),
285 (status = 404, description = "No configured Service-provided route matched", body = ErrorResponse, content_type = "application/problem+json"),
286 (status = 502, description = "Provider export request failed", body = ErrorResponse, content_type = "application/problem+json"),
287 )
288)]
289async fn proxy_delete(
290 State(ctx): State<AppContext>,
291 admin: AdminActor,
292 HttpRequestContext(request_ctx): HttpRequestContext,
293 Path((module, path)): Path<(String, String)>,
294 request: Request<Body>,
295) -> Result<Json<ProviderHttpProxyResponse>, ApiErrorResponse> {
296 let (parts, body) = request.into_parts();
297 let body = to_bytes(body, MAX_PROXY_DELETE_REQUEST_BYTES)
298 .await
299 .map_err(|error| {
300 ApiErrorResponse::with_context(
301 AppError::new(
302 ErrorCode::Validation,
303 format!("provider HTTP proxy DELETE request body could not be read: {error}"),
304 ),
305 &request_ctx,
306 )
307 })?;
308 if !body.is_empty() {
309 return Err(ApiErrorResponse::with_context(
310 AppError::new(
311 ErrorCode::Validation,
312 "provider HTTP proxy DELETE request body must be empty",
313 ),
314 &request_ctx,
315 ));
316 }
317
318 let request_path = format!("/{path}");
319 let matched = provider_http_proxy_registry()
320 .match_route(&module, ModuleHttpMethod::Delete, &request_path)
321 .ok_or_else(|| {
322 ApiErrorResponse::with_context(
323 AppError::new(
324 ErrorCode::NotFound,
325 format!("provider HTTP route not found: {module}{request_path}"),
326 ),
327 &request_ctx,
328 )
329 })?;
330
331 ensure_capability(&admin, &matched, &request_ctx)?;
332 let data = forward_delete(&ctx, &matched, &parts.headers, &request_ctx).await?;
333 Ok(Json(ProviderHttpProxyResponse::from_match(matched, data)))
334}
335
336async fn proxy_body_method(
337 method: ModuleHttpMethod,
338 ctx: AppContext,
339 admin: AdminActor,
340 request_ctx: platform_core::RequestContext,
341 headers: HeaderMap,
342 module: String,
343 path: String,
344 body: Bytes,
345) -> Result<Json<ProviderHttpProxyResponse>, ApiErrorResponse> {
346 let request_path = format!("/{path}");
347 let matched = provider_http_proxy_registry()
348 .match_route(&module, method, &request_path)
349 .ok_or_else(|| {
350 ApiErrorResponse::with_context(
351 AppError::new(
352 ErrorCode::NotFound,
353 format!("provider HTTP route not found: {module}{request_path}"),
354 ),
355 &request_ctx,
356 )
357 })?;
358
359 ensure_capability(&admin, &matched, &request_ctx)?;
360 let data = forward_body_method(method, &ctx, &matched, &headers, body, &request_ctx).await?;
361 Ok(Json(ProviderHttpProxyResponse::from_match(matched, data)))
362}
363
364fn ensure_capability(
365 admin: &AdminActor,
366 matched: &ProviderHttpProxyMatch,
367 request_ctx: &platform_core::RequestContext,
368) -> Result<(), ApiErrorResponse> {
369 let Some(capability) = matched.capability.as_deref() else {
370 return Err(ApiErrorResponse::with_context(
371 AppError::new(
372 ErrorCode::Forbidden,
373 "provider HTTP route has no declared capability",
374 ),
375 request_ctx,
376 ));
377 };
378
379 match admin {
380 AdminActor::System => Ok(()),
381 AdminActor::Service { scopes, .. } | AdminActor::User { scopes, .. }
382 if scopes.iter().any(|scope| scope == capability) =>
383 {
384 Ok(())
385 }
386 AdminActor::Service { .. } | AdminActor::User { .. } => {
387 Err(ApiErrorResponse::with_context(
388 AppError::new(
389 ErrorCode::Forbidden,
390 format!("missing provider HTTP route capability: {capability}"),
391 ),
392 request_ctx,
393 ))
394 }
395 }
396}
397
398#[derive(Debug, Clone)]
399struct ProxyForwardRequest<'a> {
400 ctx: &'a AppContext,
401 matched: &'a ProviderHttpProxyMatch,
402 method: ModuleHttpMethod,
403 headers: &'a HeaderMap,
404 request_ctx: &'a platform_core::RequestContext,
405 body: ProxyRequestBody,
406}
407
408async fn forward_get(
409 ctx: &AppContext,
410 matched: &ProviderHttpProxyMatch,
411 headers: &HeaderMap,
412 request_ctx: &platform_core::RequestContext,
413) -> Result<Value, ApiErrorResponse> {
414 forward_proxy_request(ProxyForwardRequest {
415 ctx,
416 matched,
417 method: ModuleHttpMethod::Get,
418 headers,
419 request_ctx,
420 body: ProxyRequestBody::Empty,
421 })
422 .await
423}
424
425async fn forward_body_method(
426 method: ModuleHttpMethod,
427 ctx: &AppContext,
428 matched: &ProviderHttpProxyMatch,
429 headers: &HeaderMap,
430 body: Bytes,
431 request_ctx: &platform_core::RequestContext,
432) -> Result<Value, ApiErrorResponse> {
433 forward_proxy_request(ProxyForwardRequest {
434 ctx,
435 matched,
436 method,
437 headers,
438 request_ctx,
439 body: ProxyRequestBody::Json(body),
440 })
441 .await
442}
443
444async fn forward_delete(
445 ctx: &AppContext,
446 matched: &ProviderHttpProxyMatch,
447 headers: &HeaderMap,
448 request_ctx: &platform_core::RequestContext,
449) -> Result<Value, ApiErrorResponse> {
450 forward_proxy_request(ProxyForwardRequest {
451 ctx,
452 matched,
453 method: ModuleHttpMethod::Delete,
454 headers,
455 request_ctx,
456 body: ProxyRequestBody::Empty,
457 })
458 .await
459}
460
461async fn forward_proxy_request(
462 request: ProxyForwardRequest<'_>,
463) -> Result<Value, ApiErrorResponse> {
464 forward_provider_invocation(request).await
465}
466
467async fn forward_provider_invocation(
468 request: ProxyForwardRequest<'_>,
469) -> Result<Value, ApiErrorResponse> {
470 let started_at = Instant::now();
471 let matched = request.matched;
472 let request_ctx = request.request_ctx;
473 let parts =
474 apply_grpc_proxy_request_policy(request.method, request.headers, request_ctx, request.body)
475 .map_err(|error| ApiErrorResponse::with_context(error, request_ctx))?;
476 let payload = ProviderHttpProxyInvokeRequest {
477 request_id: request_ctx.request_id.0.clone(),
478 correlation_id: request_ctx.correlation_id.0.clone(),
479 module_name: matched.module_name.clone(),
480 method: module_http_method_label(request.method).to_owned(),
481 declared_path: matched.declared_path.clone(),
482 provider_path: matched.provider_path.clone(),
483 path_params: matched.path_params.clone(),
484 headers: parts.headers,
485 body: parts.body,
486 };
487 let request_body_evidence = capture_json_body(payload.body.as_ref(), "method_without_body");
488 let invocation = invocation::build(
489 &matched.config,
490 ProviderOperationKind::HttpRoute,
491 format!("{} {}", payload.method, payload.declared_path),
492 "1",
493 if matches!(request.method, ModuleHttpMethod::Get) {
494 ProviderInvocationMode::ReadOnly
495 } else {
496 ProviderInvocationMode::Durable
497 },
498 InvocationContext {
499 invocation_id: request_ctx.request_id.0.clone(),
500 request_id: request_ctx.request_id.0.clone(),
501 attempt: 1,
502 actor: request_ctx.actor.clone(),
503 tenant_id: request_ctx
504 .tenant_id
505 .as_ref()
506 .map(|tenant| tenant.0.clone()),
507 correlation_id: request_ctx.correlation_id.0.clone(),
508 causation_id: request_ctx.causation_id.clone(),
509 trace: request_ctx.trace.clone(),
510 },
511 serde_json::to_value(payload).map_err(|error| {
512 ApiErrorResponse::with_context(
513 AppError::new(
514 ErrorCode::Internal,
515 format!("encode Provider payload: {error}"),
516 ),
517 request_ctx,
518 )
519 })?,
520 )
521 .map_err(|error| ApiErrorResponse::with_context(error, request_ctx))?;
522 let client = reqwest::Client::builder()
523 .timeout(Duration::from_millis(matched.timeout_ms))
524 .build()
525 .map_err(|error| {
526 ApiErrorResponse::with_context(
527 AppError::new(
528 ErrorCode::Internal,
529 format!("build Provider client: {error}"),
530 ),
531 request_ctx,
532 )
533 })?;
534 let effects = crate::ProviderHostEffectCoordinator::new(request.ctx.db.clone());
535 let outcome = invocation::send(
536 &client,
537 &matched.config,
538 &effects,
539 "http:invoke",
540 &invocation,
541 )
542 .await
543 .map_err(|error| ApiErrorResponse::with_context(error, request_ctx))?;
544 let value = invocation::result(&invocation, outcome)
545 .map_err(|error| ApiErrorResponse::with_context(error, request_ctx))?;
546 let (response, provider_status) = decode_http_proxy_result(value)
547 .map_err(|error| ApiErrorResponse::with_context(error, request_ctx))?;
548 let response_body_evidence = capture_json_body(response.body.as_ref(), "empty_response_body");
549 record_proxy_call(
550 request.ctx,
551 matched,
552 request_ctx,
553 started_at,
554 Some(provider_status),
555 None,
556 ProviderHttpCallBodyEvidence {
557 request: request_body_evidence,
558 response: response_body_evidence,
559 },
560 )
561 .await;
562 Ok(response.body.unwrap_or(Value::Null))
563}
564
565async fn record_proxy_call(
566 ctx: &AppContext,
567 matched: &ProviderHttpProxyMatch,
568 request_ctx: &platform_core::RequestContext,
569 started_at: Instant,
570 provider_status: Option<reqwest::StatusCode>,
571 error: Option<&AppError>,
572 body_evidence: ProviderHttpCallBodyEvidence,
573) {
574 let duration_ms = started_at.elapsed().as_millis().min(i64::MAX as u128) as i64;
575 match error {
576 Some(error) => {
577 tracing::warn!(
578 module_name = %matched.module_name,
579 declared_path = %matched.declared_path,
580 provider_path = %matched.provider_path,
581 http_method = %module_http_method_label(matched.method),
582 provider_status = provider_status.map_or(0, |status| status.as_u16()),
583 duration_ms,
584 error_code = error.code.as_str(),
585 retryable = error.retryable,
586 request_id = %request_ctx.request_id.0,
587 correlation_id = %request_ctx.correlation_id.0,
588 "provider HTTP proxy call failed"
589 );
590 }
591 None => {
592 tracing::info!(
593 module_name = %matched.module_name,
594 declared_path = %matched.declared_path,
595 provider_path = %matched.provider_path,
596 http_method = %module_http_method_label(matched.method),
597 provider_status = provider_status.map_or(0, |status| status.as_u16()),
598 duration_ms,
599 request_id = %request_ctx.request_id.0,
600 correlation_id = %request_ctx.correlation_id.0,
601 "provider HTTP proxy call completed"
602 );
603 }
604 }
605
606 let record = ProviderHttpCallRecord {
607 module_name: matched.module_name.clone(),
608 method: module_http_method_label(matched.method).to_owned(),
609 declared_path: matched.declared_path.clone(),
610 provider_path: matched.provider_path.clone(),
611 capability: matched.capability.clone(),
612 display_name: matched.display_name.clone(),
613 story_title: matched.story_title.clone(),
614 provider_status: provider_status.map(|status| status.as_u16()),
615 duration_ms,
616 success: error.is_none(),
617 error_code: error.map(|error| error.code.as_str().to_owned()),
618 retryable: error.is_some_and(|error| error.retryable),
619 path_params: json!(matched.path_params),
620 error_details: error
621 .map(|error| json!(error.details))
622 .unwrap_or_else(|| Value::Array(Vec::new())),
623 };
624
625 if let Err(error) = insert_provider_http_call_with_body_evidence(
626 &ctx.db,
627 ctx.ids.as_ref(),
628 request_ctx,
629 record,
630 body_evidence,
631 )
632 .await
633 {
634 tracing::warn!(
635 error = ?error,
636 module_name = %matched.module_name,
637 declared_path = %matched.declared_path,
638 provider_path = %matched.provider_path,
639 http_method = %module_http_method_label(matched.method),
640 request_id = %request_ctx.request_id.0,
641 correlation_id = %request_ctx.correlation_id.0,
642 "failed to persist provider HTTP proxy call"
643 );
644 }
645}
646
647fn decode_http_proxy_result(
648 value: Value,
649) -> platform_core::AppResult<(ProviderHttpProxyInvokeResponse, reqwest::StatusCode)> {
650 let response: ProviderHttpProxyInvokeResponse =
651 serde_json::from_value(value).map_err(|error| {
652 AppError::new(
653 ErrorCode::ExternalDependency,
654 format!("Provider HTTP result violated its contract: {error}"),
655 )
656 })?;
657 let provider_status = reqwest::StatusCode::from_u16(response.status_code)
658 .map_err(|error| AppError::new(ErrorCode::ExternalDependency, error.to_string()))?;
659 Ok((response, provider_status))
660}
661
662fn module_http_method_label(method: ModuleHttpMethod) -> &'static str {
663 match method {
664 ModuleHttpMethod::Get => "GET",
665 ModuleHttpMethod::Post => "POST",
666 ModuleHttpMethod::Put => "PUT",
667 ModuleHttpMethod::Patch => "PATCH",
668 ModuleHttpMethod::Delete => "DELETE",
669 _ => "UNKNOWN",
670 }
671}
672
673impl ProviderHttpProxyResponse {
674 fn from_match(matched: ProviderHttpProxyMatch, data: Value) -> Self {
675 Self {
676 status: ProviderHttpProxyStatus::Forwarded,
677 module_name: matched.module_name,
678 method: matched.method,
679 declared_path: matched.declared_path,
680 provider_path: matched.provider_path,
681 capability: matched.capability.unwrap_or_default(),
682 path_params: matched.path_params,
683 data,
684 }
685 }
686}