| 459 | } |
| 460 | |
| 461 | fn validate_vertex_base_url(value: &str) -> Result<String, Status> { |
| 462 | let trimmed = value.trim(); |
| 463 | let url = url::Url::parse(trimmed).map_err(|err| { |
| 464 | Status::invalid_argument(format!("Vertex AI base URL override is invalid: {err}")) |
| 465 | })?; |
| 466 | |
| 467 | if url.scheme() != "https" { |
| 468 | return Err(Status::invalid_argument( |
| 469 | "Vertex AI base URL override must use https".to_string(), |
| 470 | )); |
| 471 | } |
| 472 | if !url.username().is_empty() || url.password().is_some() { |
| 473 | return Err(Status::invalid_argument( |
| 474 | "Vertex AI base URL override must not include userinfo".to_string(), |
| 475 | )); |
| 476 | } |
| 477 | if url.query().is_some() || url.fragment().is_some() { |
| 478 | return Err(Status::invalid_argument( |
| 479 | "Vertex AI base URL override must not include query or fragment components".to_string(), |
| 480 | )); |
| 481 | } |
| 482 | if let Some(port) = url.port() |
| 483 | && port != 443 |
| 484 | { |
| 485 | return Err(Status::invalid_argument(format!( |
| 486 | "Vertex AI base URL override must use port 443 when an explicit port is set, got {port}" |
| 487 | ))); |
| 488 | } |
| 489 | |
| 490 | match url.host() { |
| 491 | Some(url::Host::Domain(host)) if is_allowed_vertex_override_host(host) => {} |
| 492 | Some(url::Host::Domain(host)) => { |
| 493 | return Err(Status::invalid_argument(format!( |
| 494 | "Vertex AI base URL override must target an official Vertex AI hostname, got {host:?}" |
| 495 | ))); |
| 496 | } |
| 497 | Some(url::Host::Ipv4(_) | url::Host::Ipv6(_)) => { |
| 498 | return Err(Status::invalid_argument(format!( |
| 499 | "Vertex AI base URL override must not use IP literal hosts: {}", |
| 500 | url.host_str().unwrap_or("<unknown>") |
| 501 | ))); |
| 502 | } |
| 503 | None => { |
| 504 | return Err(Status::invalid_argument( |
| 505 | "Vertex AI base URL override must include a host".to_string(), |
| 506 | )); |
| 507 | } |
| 508 | } |
| 509 | |
| 510 | Ok(trimmed.to_string()) |
| 511 | } |
| 512 | |
| 513 | /// Build a [`RouterResolvedRoute`] for Vertex AI without duplicating the 15-field struct. |
| 514 | #[allow(clippy::too_many_arguments)] |