Files
modrinth/packages/app-lib/src/api/instance/shared/client.rs
T

1066 lines
29 KiB
Rust

use super::install::*;
use super::*;
#[derive(Clone, Copy, Debug)]
pub(super) enum SharedInstancesRequestAuth {
ModrinthSession,
None,
}
impl SharedInstancesRequestAuth {
pub(super) fn label(self) -> &'static str {
match self {
Self::ModrinthSession => "modrinth_session",
Self::None => "none",
}
}
}
impl SharedInstanceUnavailableReason {
pub(super) fn from_status(status: StatusCode) -> Option<Self> {
match status {
StatusCode::NOT_FOUND => Some(Self::Deleted),
StatusCode::UNAUTHORIZED => Some(Self::AccessRevoked),
_ => None,
}
}
}
#[derive(Debug)]
pub(super) enum SharedInstanceRemoteResponse<T> {
Available(T),
Unavailable(SharedInstanceUnavailableReason),
}
#[derive(Clone, Debug, Deserialize)]
pub(super) struct RemoteInstanceResponse {
#[serde(default)]
pub(super) quarantine: bool,
}
#[derive(Clone, Debug)]
pub(super) struct ExternalFileCandidate {
pub(super) file_name: String,
pub(super) file_type: String,
pub(super) source: ExternalFileSource,
}
#[derive(Clone, Debug)]
pub(super) enum ExternalFileSource {
InstanceFile(String),
ConfigBundle(Vec<u8>),
}
#[derive(Clone, Debug, Serialize, Deserialize)]
pub(super) struct ConfigFile {
pub(super) path: String,
}
#[derive(Clone, Debug, Serialize)]
pub(super) struct ExternalFileData {
pub(super) file_name: String,
pub(super) file_type: String,
}
#[derive(Clone, Debug, Deserialize)]
pub(super) struct CreateInstanceResponse {
#[serde(alias = "instance_id")]
pub(super) id: String,
}
#[derive(Clone, Debug, Deserialize)]
pub(super) struct CreateInstanceInviteResponse {
pub(super) id: String,
}
#[derive(Clone, Debug, Deserialize)]
pub(super) struct InstanceInviteResponse {
pub(super) id: String,
pub(super) expiration: DateTime<Utc>,
pub(super) max_uses: i32,
pub(super) uses: i32,
}
#[derive(Clone, Debug, Deserialize)]
pub(super) struct BlacklistStatusResponse {
pub(super) blacklisted: bool,
}
#[derive(Clone, Debug, Deserialize)]
pub(super) struct InstanceInviteInfoResponse {
pub(super) instance_id: String,
pub(super) instance_name: String,
#[serde(default)]
pub(super) instance_icon: Option<String>,
#[serde(default)]
pub(super) managers: Vec<InstanceInviteManagerResponse>,
}
#[derive(Clone, Debug, Deserialize)]
#[serde(tag = "type", rename_all = "snake_case")]
pub(super) enum InstanceInviteManagerResponse {
User { id: String },
Server { name: String, icon: Option<String> },
}
#[derive(Clone, Debug, Deserialize)]
pub(super) struct InstanceVersionResponse {
pub(super) version: i32,
#[serde(default)]
pub(super) modrinth_ids: Vec<String>,
pub(super) ready: bool,
#[serde(default)]
pub(super) external_files: Vec<ExternalFileResponse>,
#[serde(default)]
pub(super) modpack_id: Option<String>,
pub(super) game_version: String,
pub(super) loader: ModLoader,
pub(super) loader_version: String,
}
#[derive(Clone, Debug, Deserialize)]
pub(super) struct ExternalFileResponse {
pub(super) file_name: String,
pub(super) file_type: String,
pub(super) url: String,
#[serde(default)]
pub(super) file_size: Option<i64>,
}
#[derive(Clone, Debug, Deserialize)]
pub(super) struct RemoteSharedInstanceUsers {
#[serde(default)]
pub(super) users: Vec<SharedInstanceUser>,
#[serde(default)]
pub(super) tokens: i32,
}
#[derive(Clone, Debug, Deserialize)]
#[serde(untagged)]
pub(super) enum RemoteSharedInstanceUsersResponse {
Current(RemoteSharedInstanceUsers),
Legacy(Vec<String>),
}
impl RemoteSharedInstanceUsersResponse {
pub(super) fn into_shared_users(self) -> SharedInstanceUsers {
match self {
Self::Current(response) => {
SharedInstanceUsers::from_users(response.users, response.tokens)
}
Self::Legacy(user_ids) => {
SharedInstanceUsers::from_user_ids(user_ids)
}
}
}
}
pub(super) async fn create_remote_instance(
name: String,
state: &State,
) -> crate::Result<CreateInstanceResponse> {
request_json(
"create_instance",
Method::POST,
"/instances",
Some(json!({ "name": name })),
state,
)
.await
}
pub(super) async fn get_user_blacklist_status(
user_id: &str,
state: &State,
) -> crate::Result<BlacklistStatusResponse> {
request_json(
"get_user_blacklist_status",
Method::GET,
&format!("/blacklist/{user_id}"),
None,
state,
)
.await
}
pub(super) async fn delete_remote_instance(
shared_instance_id: &str,
state: &State,
) -> crate::Result<()> {
let operation = "delete_instance";
let method = Method::DELETE;
let path = format!("/instances/{shared_instance_id}");
let response =
send_request(operation, method.clone(), &path, None, state).await?;
if response.status().is_success()
|| matches!(response.status(), StatusCode::NOT_FOUND | StatusCode::GONE)
{
return Ok(());
}
shared_instances_request_error(operation, method, &path, response).await
}
pub(super) async fn update_remote_instance(
shared_instance_id: &str,
name: String,
state: &State,
) -> crate::Result<()> {
let operation = "update_instance";
let method = Method::PATCH;
let path = format!("/instances/{shared_instance_id}");
let response = send_request(
operation,
method.clone(),
&path,
Some(json!({ "name": name })),
state,
)
.await?;
if response.status().is_success() {
return Ok(());
}
if response.status() == StatusCode::METHOD_NOT_ALLOWED {
let request_id = response_request_id(&response);
tracing::warn!(
operation,
method = method.as_str(),
path,
status = StatusCode::METHOD_NOT_ALLOWED.as_u16(),
request_id = request_id.as_deref().unwrap_or("none"),
"Shared instances API does not support remote instance updates; skipping name sync"
);
return Ok(());
}
shared_instances_request_error(operation, method, &path, response).await
}
pub(super) async fn get_remote_instance_access(
shared_instance_id: &str,
state: &State,
) -> crate::Result<SharedInstanceRemoteResponse<()>> {
let operation = "get_instance";
let method = Method::GET;
let path = format!("/instances/{shared_instance_id}");
let response =
send_request(operation, method.clone(), &path, None, state).await?;
if let Some(reason) =
SharedInstanceUnavailableReason::from_status(response.status())
{
if reason == SharedInstanceUnavailableReason::AccessRevoked
&& !active_modrinth_session_is_valid(state).await?
{
return Err(crate::ErrorKind::NoCredentialsError.into());
}
return Ok(SharedInstanceRemoteResponse::Unavailable(reason));
}
if !response.status().is_success() {
return shared_instances_request_error(
operation, method, &path, response,
)
.await;
}
let instance = decode_json_response::<RemoteInstanceResponse>(
operation, method, &path, response,
)
.await?;
if instance.quarantine {
return Ok(SharedInstanceRemoteResponse::Unavailable(
SharedInstanceUnavailableReason::Quarantined,
));
}
Ok(SharedInstanceRemoteResponse::Available(()))
}
pub(super) async fn update_remote_instance_icon(
shared_instance_id: &str,
icon_path: Option<&str>,
state: &State,
) -> crate::Result<()> {
let path = format!("/instances/{shared_instance_id}/icon");
let Some(icon_path) = icon_path else {
return request_empty(
"delete_instance_icon",
Method::DELETE,
&path,
None,
state,
)
.await;
};
let bytes = crate::util::io::read(icon_path).await?;
let operation = "upload_instance_icon";
let method = Method::PUT;
let response =
send_bytes_request(operation, method.clone(), &path, bytes, state)
.await?;
if response.status().is_success() {
return Ok(());
}
shared_instances_request_error(operation, method, &path, response).await
}
pub(super) async fn get_remote_users(
shared_instance_id: &str,
state: &State,
) -> crate::Result<SharedInstanceUsers> {
let users = request_json::<RemoteSharedInstanceUsersResponse>(
"get_instance_users",
Method::GET,
&format!("/instances/{shared_instance_id}/users"),
None,
state,
)
.await?;
Ok(users.into_shared_users())
}
pub(super) async fn get_latest_remote_version(
shared_instance_id: &str,
state: &State,
) -> crate::Result<InstanceVersionResponse> {
match get_latest_remote_version_optional_unavailable(
shared_instance_id,
state,
)
.await?
{
SharedInstanceRemoteResponse::Available(version) => Ok(version),
SharedInstanceRemoteResponse::Unavailable(
reason @ SharedInstanceUnavailableReason::AccessRevoked,
) => {
if !accept_pending_remote_invite(shared_instance_id, state).await? {
return Err(shared_instance_unavailable_error(reason));
}
match get_latest_remote_version_optional_unavailable(
shared_instance_id,
state,
)
.await?
{
SharedInstanceRemoteResponse::Available(version) => Ok(version),
SharedInstanceRemoteResponse::Unavailable(reason) => {
Err(shared_instance_unavailable_error(reason))
}
}
}
SharedInstanceRemoteResponse::Unavailable(reason) => {
Err(shared_instance_unavailable_error(reason))
}
}
}
pub(super) async fn get_latest_remote_version_optional_unavailable(
shared_instance_id: &str,
state: &State,
) -> crate::Result<SharedInstanceRemoteResponse<InstanceVersionResponse>> {
get_latest_remote_version_optional_unavailable_with_auth(
shared_instance_id,
state,
SharedInstancesRequestAuth::ModrinthSession,
)
.await
}
pub(super) async fn get_latest_remote_version_optional_unavailable_with_auth(
shared_instance_id: &str,
state: &State,
auth: SharedInstancesRequestAuth,
) -> crate::Result<SharedInstanceRemoteResponse<InstanceVersionResponse>> {
request_json_optional_unavailable(
"get_latest_instance_version",
Method::GET,
&format!("/instances/{shared_instance_id}/versions"),
None,
state,
auth,
)
.await
}
pub(super) async fn add_remote_users(
shared_instance_id: &str,
user_ids: Vec<String>,
state: &State,
) -> crate::Result<()> {
request_empty(
"add_instance_users",
Method::POST,
&format!("/instances/{shared_instance_id}/users"),
Some(json!({ "user_ids": user_ids })),
state,
)
.await
}
pub(super) async fn remove_remote_users(
shared_instance_id: &str,
user_ids: Vec<String>,
state: &State,
) -> crate::Result<()> {
request_empty(
"remove_instance_users",
Method::DELETE,
&format!("/instances/{shared_instance_id}/users"),
Some(json!({ "user_ids": user_ids })),
state,
)
.await
}
pub(super) async fn accept_pending_remote_invite(
shared_instance_id: &str,
state: &State,
) -> crate::Result<bool> {
let operation = "accept_pending_instance_invite";
let method = Method::POST;
let path = format!("/instances/{shared_instance_id}/invites/pending");
let response =
send_request(operation, method.clone(), &path, None, state).await?;
match response.status() {
StatusCode::OK | StatusCode::NO_CONTENT => Ok(true),
StatusCode::NOT_FOUND => Ok(false),
status if status.is_success() => Ok(true),
_ => {
shared_instances_request_error(operation, method, &path, response)
.await
}
}
}
pub(super) async fn accept_shared_instance_invite(
shared_instance_id: &str,
invite_id: &str,
state: &State,
) -> crate::Result<()> {
let path = format!("/instances/{shared_instance_id}/invites/{invite_id}");
let log_path = "/instances/:instance_id/invites/:invite_id";
let operation = "accept_instance_invite";
let method = Method::POST;
let response = send_request_with_auth_and_log_path(
operation,
method.clone(),
&path,
log_path,
None,
state,
SharedInstancesRequestAuth::ModrinthSession,
)
.await?;
if response.status().is_success() {
return Ok(());
}
let status = response.status();
let request_id = response_request_id(&response);
let body = response
.text()
.await
.map_err(reqwest::Error::without_url)
.unwrap_or_default();
if status == StatusCode::BAD_REQUEST && body.contains("already has access")
{
return Ok(());
}
tracing::warn!(
operation,
method = method.as_str(),
path = log_path,
status = status.as_u16(),
request_id = request_id.as_deref().unwrap_or("none"),
"Shared instances API request failed"
);
Err(crate::ErrorKind::OtherError(format!(
"Shared instances API request {operation} {method} {log_path} failed with status {status}"
))
.into())
}
pub(super) async fn delete_remote_invite(
shared_instance_id: &str,
invite_id: &str,
state: &State,
) -> crate::Result<()> {
let path = format!("/instances/{shared_instance_id}/invites/{invite_id}");
request_empty_with_log_path(
"delete_instance_invite",
Method::DELETE,
&path,
"/instances/:instance_id/invites/:invite_id",
None,
state,
)
.await
}
pub(super) async fn get_remote_invites(
shared_instance_id: &str,
state: &State,
) -> crate::Result<Vec<InstanceInviteResponse>> {
request_json(
"get_instance_invites",
Method::GET,
&format!("/instances/{shared_instance_id}/invites"),
None,
state,
)
.await
}
pub(super) async fn get_shared_instance_invite_info(
invite_id: &str,
state: &State,
) -> crate::Result<InstanceInviteInfoResponse> {
let operation = "get_instance_invite";
let method = Method::GET;
let path = format!("/invites/{invite_id}");
let log_path = "/invites/:invite_id";
let response = send_request_with_auth_and_log_path(
operation,
method.clone(),
&path,
log_path,
None,
state,
SharedInstancesRequestAuth::None,
)
.await?;
if !response.status().is_success() {
return shared_instances_request_error(
operation, method, log_path, response,
)
.await;
}
decode_json_response_with_log_path(
operation, method, log_path, response, true,
)
.await
}
pub(super) async fn decline_pending_remote_invite(
shared_instance_id: &str,
state: &State,
) -> crate::Result<()> {
request_empty(
"decline_pending_instance_invite",
Method::DELETE,
&format!("/instances/{shared_instance_id}/invites/pending"),
None,
state,
)
.await
}
pub(super) async fn request_json<T>(
operation: &'static str,
method: Method,
path: &str,
body: Option<serde_json::Value>,
state: &State,
) -> crate::Result<T>
where
T: DeserializeOwned,
{
let response =
request(operation, method.clone(), path, body, state).await?;
decode_json_response(operation, method, path, response).await
}
pub(super) async fn request_json_optional_unavailable<T>(
operation: &'static str,
method: Method,
path: &str,
body: Option<serde_json::Value>,
state: &State,
auth: SharedInstancesRequestAuth,
) -> crate::Result<SharedInstanceRemoteResponse<T>>
where
T: DeserializeOwned,
{
let response = send_request_with_auth(
operation,
method.clone(),
path,
body,
state,
auth,
)
.await?;
if let Some(reason) =
SharedInstanceUnavailableReason::from_status(response.status())
{
if reason == SharedInstanceUnavailableReason::AccessRevoked
&& matches!(auth, SharedInstancesRequestAuth::ModrinthSession)
&& !active_modrinth_session_is_valid(state).await?
{
tracing::warn!(
operation,
method = method.as_str(),
path,
status = response.status().as_u16(),
"Shared instances API returned unauthorized while Modrinth auth is unavailable"
);
return Err(crate::ErrorKind::NoCredentialsError.into());
}
tracing::warn!(
operation,
method = method.as_str(),
path,
status = response.status().as_u16(),
"Shared instances API resource is unavailable"
);
return Ok(SharedInstanceRemoteResponse::Unavailable(reason));
}
if !response.status().is_success() {
return shared_instances_request_error(
operation, method, path, response,
)
.await;
}
decode_json_response(operation, method, path, response)
.await
.map(SharedInstanceRemoteResponse::Available)
}
pub(super) async fn active_modrinth_session_is_valid(
state: &State,
) -> crate::Result<bool> {
let Some(credentials) =
ModrinthCredentials::get_and_refresh(&state.pool, &state.api_semaphore)
.await?
else {
return Ok(false);
};
let _permit = state.api_semaphore.0.acquire().await?;
let response = INSECURE_REQWEST_CLIENT
.get(concat!(env!("MODRINTH_API_URL"), "user"))
.header("Authorization", &credentials.session)
.send()
.await?;
if response.status() == StatusCode::UNAUTHORIZED {
ModrinthCredentials::remove(&credentials.user_id, &state.pool).await?;
return Ok(false);
}
if response.status().is_success() {
return Ok(true);
}
let status = response.status();
let request_id = response_request_id(&response);
tracing::warn!(
operation = "validate_modrinth_session",
method = Method::GET.as_str(),
path = "/user",
status = status.as_u16(),
request_id = request_id.as_deref().unwrap_or("none"),
"Modrinth auth validation request failed"
);
Err(crate::ErrorKind::OtherError(format!(
"Modrinth auth validation failed with status {status}"
))
.into())
}
pub(super) async fn decode_json_response<T>(
operation: &'static str,
method: Method,
path: &str,
response: reqwest::Response,
) -> crate::Result<T>
where
T: DeserializeOwned,
{
decode_json_response_with_log_path(operation, method, path, response, false)
.await
}
async fn decode_json_response_with_log_path<T>(
operation: &'static str,
method: Method,
log_path: &str,
response: reqwest::Response,
strip_response_url: bool,
) -> crate::Result<T>
where
T: DeserializeOwned,
{
let status = response.status();
let request_id = response_request_id(&response);
let body = match response.text().await {
Ok(body) => body,
Err(error) if strip_response_url => {
return Err(crate::ErrorKind::SharedInstancesApiError(
error.without_url().to_string(),
)
.into());
}
Err(error) => {
return Err(crate::ErrorKind::SharedInstancesApiError(
error.to_string(),
)
.into());
}
};
serde_json::from_str::<T>(&body).map_err(|error| {
tracing::warn!(
operation,
method = method.as_str(),
path = log_path,
status = status.as_u16(),
request_id = request_id.as_deref().unwrap_or("none"),
error_category = ?error.classify(),
error_line = error.line(),
error_column = error.column(),
"Shared instances API returned an invalid JSON response"
);
crate::ErrorKind::SharedInstancesApiError(format!(
"Shared instances API request {operation} {method} {log_path} returned invalid JSON with status {status}"
))
.into()
})
}
pub(super) async fn request_empty(
operation: &'static str,
method: Method,
path: &str,
body: Option<serde_json::Value>,
state: &State,
) -> crate::Result<()> {
request(operation, method, path, body, state).await?;
Ok(())
}
async fn request_empty_with_log_path(
operation: &'static str,
method: Method,
path: &str,
log_path: &str,
body: Option<serde_json::Value>,
state: &State,
) -> crate::Result<()> {
let response = send_request_with_auth_and_log_path(
operation,
method.clone(),
path,
log_path,
body,
state,
SharedInstancesRequestAuth::ModrinthSession,
)
.await?;
if response.status().is_success() {
return Ok(());
}
shared_instances_request_error(operation, method, log_path, response).await
}
pub(super) async fn request(
operation: &'static str,
method: Method,
path: &str,
body: Option<serde_json::Value>,
state: &State,
) -> crate::Result<reqwest::Response> {
let response =
send_request(operation, method.clone(), path, body, state).await?;
if response.status().is_success() {
return Ok(response);
}
shared_instances_request_error(operation, method, path, response).await
}
pub(super) async fn send_request(
operation: &'static str,
method: Method,
path: &str,
body: Option<serde_json::Value>,
state: &State,
) -> crate::Result<reqwest::Response> {
send_request_with_auth(
operation,
method,
path,
body,
state,
SharedInstancesRequestAuth::ModrinthSession,
)
.await
}
pub(super) async fn send_bytes_request(
operation: &'static str,
method: Method,
path: &str,
body: Vec<u8>,
state: &State,
) -> crate::Result<reqwest::Response> {
let base_url = service_base_url();
let url = service_url(base_url, path);
send_bytes_request_to_url(operation, method, path, &url, body, None, state)
.await
}
pub(super) async fn send_bytes_request_to_url(
operation: &'static str,
method: Method,
path: &str,
url: &str,
body: Vec<u8>,
file_sha512: Option<&str>,
state: &State,
) -> crate::Result<reqwest::Response> {
let service_origin = url::Url::parse(service_base_url())
.map_err(|error| {
crate::ErrorKind::OtherError(format!(
"Invalid shared instances API base URL: {error}"
))
})?
.origin();
let upload_origin = url::Url::parse(url)
.map_err(|error| {
crate::ErrorKind::OtherError(format!(
"Invalid shared instances upload URL: {error}"
))
})?
.origin();
if service_origin != upload_origin {
return Err(crate::ErrorKind::OtherError(
"Shared instances upload URL has an unexpected origin".to_string(),
)
.into());
}
let credentials =
ModrinthCredentials::get_and_refresh(&state.pool, &state.api_semaphore)
.await?
.ok_or(crate::ErrorKind::NoCredentialsError)?;
let _permit = state.api_semaphore.0.acquire().await?;
tracing::debug!(
operation,
method = method.as_str(),
path,
url = %url,
user_id = credentials.user_id.as_str(),
auth = SharedInstancesRequestAuth::ModrinthSession.label(),
has_body = true,
"Sending shared instances API request"
);
let mut request = shared_instances_client(url)
.request(method.clone(), url)
.bearer_auth(credentials.session)
.header(reqwest::header::CONTENT_TYPE, "application/octet-stream")
.body(body);
if let Some(file_sha512) = file_sha512 {
request = request.header("x-file-sha512", file_sha512);
}
let response = request.send().await.map_err(|error| {
crate::ErrorKind::SharedInstancesApiError(
error.without_url().to_string(),
)
})?;
if response.status().is_success() {
let request_id = response_request_id(&response);
tracing::debug!(
operation,
method = method.as_str(),
path,
url = %url,
status = response.status().as_u16(),
request_id = request_id.as_deref().unwrap_or("none"),
"Shared instances API request succeeded"
);
}
Ok(response)
}
pub(super) async fn send_request_with_auth(
operation: &'static str,
method: Method,
path: &str,
body: Option<serde_json::Value>,
state: &State,
auth: SharedInstancesRequestAuth,
) -> crate::Result<reqwest::Response> {
send_request_with_auth_and_log_path(
operation, method, path, path, body, state, auth,
)
.await
}
async fn send_request_with_auth_and_log_path(
operation: &'static str,
method: Method,
path: &str,
log_path: &str,
body: Option<serde_json::Value>,
state: &State,
auth: SharedInstancesRequestAuth,
) -> crate::Result<reqwest::Response> {
let modrinth_credentials =
if matches!(auth, SharedInstancesRequestAuth::ModrinthSession) {
Some(
ModrinthCredentials::get_and_refresh(
&state.pool,
&state.api_semaphore,
)
.await?
.ok_or(crate::ErrorKind::NoCredentialsError)?,
)
} else {
None
};
let _permit = state.api_semaphore.0.acquire().await?;
let base_url = service_base_url();
let url = service_url(base_url, path);
let log_url = service_url(base_url, log_path);
let mut request =
shared_instances_client(base_url).request(method.clone(), &url);
let mut user_id = None;
match auth {
SharedInstancesRequestAuth::ModrinthSession => {
let credentials = modrinth_credentials
.expect("Modrinth session credentials were loaded");
user_id = Some(credentials.user_id);
request = request.bearer_auth(credentials.session);
}
SharedInstancesRequestAuth::None => {}
}
tracing::debug!(
operation,
method = method.as_str(),
path = log_path,
url = %log_url,
user_id = user_id.as_deref(),
auth = auth.label(),
has_body = body.is_some(),
"Sending shared instances API request"
);
if let Some(body) = body {
request = request.json(&body);
}
let response = match request.send().await {
Ok(response) => response,
Err(error) if path != log_path => {
return Err(crate::ErrorKind::SharedInstancesApiError(
error.without_url().to_string(),
)
.into());
}
Err(error) => {
return Err(crate::ErrorKind::SharedInstancesApiError(
error.to_string(),
)
.into());
}
};
if response.status().is_success() {
let request_id = response_request_id(&response);
tracing::debug!(
operation,
method = method.as_str(),
path = log_path,
url = %log_url,
status = response.status().as_u16(),
request_id = request_id.as_deref().unwrap_or("none"),
"Shared instances API request succeeded"
);
}
Ok(response)
}
pub(super) async fn shared_instances_request_error<T>(
operation: &'static str,
method: Method,
path: &str,
response: reqwest::Response,
) -> crate::Result<T> {
let status = response.status();
let request_id = response_request_id(&response);
tracing::warn!(
operation,
method = method.as_str(),
path,
status = status.as_u16(),
request_id = request_id.as_deref().unwrap_or("none"),
"Shared instances API request failed"
);
let message = format!(
"Shared instances API request {operation} {method} {path} failed with status {status}"
);
if status.is_server_error() {
return Err(crate::ErrorKind::SharedInstancesApiError(message).into());
}
Err(crate::ErrorKind::OtherError(message).into())
}
pub(super) fn response_request_id(
response: &reqwest::Response,
) -> Option<String> {
let request_id = response.headers().get("x-request-id")?.to_str().ok()?;
if request_id.is_empty()
|| request_id.len() > 128
|| !request_id.chars().all(|character| {
character.is_ascii_alphanumeric() || "-_.:".contains(character)
})
{
return None;
}
Some(request_id.to_string())
}
pub(super) fn service_url(base_url: &str, path: &str) -> String {
format!("{base_url}/v1{path}")
}
pub(super) fn service_base_url() -> &'static str {
env!("SHARED_INSTANCES_API_BASE_URL").trim_end_matches('/')
}
pub(super) fn shared_instances_client(
base_url: &str,
) -> &'static reqwest::Client {
if base_url.starts_with("https://") {
&REQWEST_CLIENT
} else {
&INSECURE_REQWEST_CLIENT
}
}