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