use std::time::Duration;
use reqwest::header::{HeaderMap, HeaderName, HeaderValue, ACCEPT, AUTHORIZATION, CONTENT_TYPE};
use reqwest::{Method, Response, StatusCode};
use serde::de::DeserializeOwned;
use serde::Serialize;
use crate::client::Client;
use crate::error::{Error, Result};
use crate::retry::run_with_retry;
use crate::stream::StreamResponse;
const HTTP_REFERER: HeaderName = HeaderName::from_static("http-referer");
const X_TITLE: HeaderName = HeaderName::from_static("x-title");
const ACCEPT_JSON: &str = "application/json";
#[allow(dead_code)] pub(crate) const ACCEPT_SSE: &str = "text/event-stream";
fn base_headers(client: &Client, accept: &'static str) -> Result<HeaderMap> {
let mut h = HeaderMap::with_capacity(5);
let auth = format!("Bearer {}", client.api_key());
let mut auth_val = HeaderValue::from_str(&auth)
.map_err(|_| Error::InvalidInput("api_key is not header-safe"))?;
auth_val.set_sensitive(true);
h.insert(AUTHORIZATION, auth_val);
h.insert(CONTENT_TYPE, HeaderValue::from_static(ACCEPT_JSON));
h.insert(ACCEPT, HeaderValue::from_static(accept));
if let Some(referer) = client.referer() {
if let Ok(v) = HeaderValue::from_str(referer) {
h.insert(HTTP_REFERER, v);
}
}
if let Some(name) = client.app_name() {
if let Ok(v) = HeaderValue::from_str(name) {
h.insert(X_TITLE, v);
}
}
Ok(h)
}
fn endpoint_url(client: &Client, path: &str) -> Result<reqwest::Url> {
let trimmed = path.trim_start_matches('/');
client
.base_url()
.join(trimmed)
.map_err(|_| Error::InvalidInput("endpoint path is not valid"))
}
fn parse_retry_after(resp: &Response) -> Option<Duration> {
let v = resp.headers().get(reqwest::header::RETRY_AFTER)?;
let s = v.to_str().ok()?.trim();
s.parse::<u64>().ok().map(Duration::from_secs)
}
pub(crate) async fn execute_json<Req, Resp>(client: &Client, path: &str, body: &Req) -> Result<Resp>
where
Req: Serialize + ?Sized,
Resp: DeserializeOwned,
{
let body_bytes = serde_json::to_vec(body)?;
execute_request(client, Method::POST, path, &[], Some(body_bytes)).await
}
#[allow(dead_code)] pub(crate) async fn execute_json_get<Resp>(
client: &Client,
path: &str,
query: &[(&str, String)],
) -> Result<Resp>
where
Resp: DeserializeOwned,
{
execute_request(client, Method::GET, path, query, None).await
}
#[allow(dead_code)] pub(crate) async fn execute_json_method<Req, Resp>(
client: &Client,
method: Method,
path: &str,
body: Option<&Req>,
) -> Result<Resp>
where
Req: Serialize + ?Sized,
Resp: DeserializeOwned,
{
let body_bytes = match body {
Some(b) => Some(serde_json::to_vec(b)?),
None => None,
};
execute_request(client, method, path, &[], body_bytes).await
}
#[allow(dead_code)] pub(crate) async fn execute_bytes_post<Req>(
client: &Client,
path: &str,
body: &Req,
) -> Result<(bytes::Bytes, Option<String>)>
where
Req: Serialize + ?Sized,
{
let body_bytes = serde_json::to_vec(body)?;
let url = endpoint_url(client, path)?;
let headers = base_headers(client, ACCEPT_JSON)?;
let cfg = client.retry().clone();
run_with_retry(&cfg, || {
let url = url.clone();
let headers = headers.clone();
let body_bytes = body_bytes.clone();
async move {
let resp = client
.http()
.request(Method::POST, url)
.headers(headers)
.body(body_bytes)
.send()
.await?;
let status = resp.status();
let content_type = resp
.headers()
.get(reqwest::header::CONTENT_TYPE)
.and_then(|v| v.to_str().ok())
.map(|s| s.to_string());
if status.is_success() {
let bytes = resp.bytes().await?;
Ok((bytes, content_type))
} else {
Err(api_error_from_response(resp, status).await)
}
}
})
.await
}
#[allow(dead_code)] pub(crate) async fn execute_bytes_get(
client: &Client,
path: &str,
query: &[(&str, String)],
) -> Result<(bytes::Bytes, Option<String>)> {
let mut url = endpoint_url(client, path)?;
if !query.is_empty() {
let mut q = url.query_pairs_mut();
for (k, v) in query {
q.append_pair(k, v);
}
drop(q);
}
let headers = base_headers(client, ACCEPT_JSON)?;
let resp = client
.http()
.request(Method::GET, url)
.headers(headers)
.send()
.await?;
let status = resp.status();
let content_type = resp
.headers()
.get(reqwest::header::CONTENT_TYPE)
.and_then(|v| v.to_str().ok())
.map(|s| s.to_string());
if status.is_success() {
let bytes = resp.bytes().await?;
Ok((bytes, content_type))
} else {
Err(api_error_from_response(resp, status).await)
}
}
#[allow(dead_code)] pub(crate) async fn execute_no_content_method<Req>(
client: &Client,
method: Method,
path: &str,
body: Option<&Req>,
) -> Result<()>
where
Req: Serialize + ?Sized,
{
let body_bytes = match body {
Some(b) => Some(serde_json::to_vec(b)?),
None => None,
};
let url = endpoint_url(client, path)?;
let headers = base_headers(client, ACCEPT_JSON)?;
let cfg = client.retry().clone();
run_with_retry(&cfg, || {
let url = url.clone();
let headers = headers.clone();
let body_bytes = body_bytes.clone();
let method = method.clone();
async move {
let mut req = client.http().request(method, url).headers(headers);
if let Some(b) = body_bytes {
req = req.body(b);
}
let resp = req.send().await?;
let status = resp.status();
if status.is_success() {
Ok(())
} else {
Err(api_error_from_response(resp, status).await)
}
}
})
.await
}
async fn execute_request<Resp>(
client: &Client,
method: Method,
path: &str,
query: &[(&str, String)],
body_bytes: Option<Vec<u8>>,
) -> Result<Resp>
where
Resp: DeserializeOwned,
{
let mut url = endpoint_url(client, path)?;
if !query.is_empty() {
let mut q = url.query_pairs_mut();
for (k, v) in query {
q.append_pair(k, v);
}
drop(q);
}
let headers = base_headers(client, ACCEPT_JSON)?;
let cfg = client.retry().clone();
run_with_retry(&cfg, || {
let url = url.clone();
let headers = headers.clone();
let body_bytes = body_bytes.clone();
let method = method.clone();
async move {
let mut req = client.http().request(method, url).headers(headers);
if let Some(b) = body_bytes {
req = req.body(b);
}
let resp = req.send().await?;
let status = resp.status();
if status.is_success() {
let bytes = resp.bytes().await?;
let decoded: Resp = serde_json::from_slice(&bytes)?;
Ok(decoded)
} else {
Err(api_error_from_response(resp, status).await)
}
}
})
.await
}
#[cfg(not(target_arch = "wasm32"))]
pub(crate) async fn open_stream_bytes(
client: &Client,
path: &str,
body_bytes: Vec<u8>,
) -> Result<StreamResponse> {
let url = endpoint_url(client, path)?;
let headers = base_headers(client, ACCEPT_SSE)?;
let resp = client
.http()
.request(Method::POST, url)
.headers(headers)
.body(body_bytes)
.send()
.await?;
let status = resp.status();
if status.is_success() {
Ok(resp)
} else {
Err(api_error_from_response(resp, status).await)
}
}
#[cfg(all(target_arch = "wasm32", feature = "browser"))]
pub(crate) async fn open_stream_bytes(
client: &Client,
path: &str,
body_bytes: Vec<u8>,
) -> Result<StreamResponse> {
use gloo_net::http::Request;
let url = endpoint_url(client, path)?;
let abort = web_sys::AbortController::new()
.map_err(|error| Error::BrowserTransport(format!("{error:?}")))?;
let mut request = Request::post(url.as_str())
.header("Authorization", &format!("Bearer {}", client.api_key()))
.header("Content-Type", ACCEPT_JSON)
.header("Accept", ACCEPT_SSE)
.abort_signal(Some(&abort.signal()));
if let Some(referer) = client.referer() {
request = request.header("HTTP-Referer", referer);
}
if let Some(name) = client.app_name() {
request = request.header("X-Title", name);
}
let body = js_sys::Uint8Array::from(body_bytes.as_slice());
let response = request
.body(body)
.map_err(|error| Error::BrowserTransport(error.to_string()))?
.send()
.await
.map_err(|error| Error::BrowserTransport(error.to_string()))?;
let status = response.status();
if (200..=299).contains(&status) {
Ok(StreamResponse::new(response, abort))
} else {
let body = response
.binary()
.await
.map_err(|error| Error::BrowserTransport(error.to_string()))?;
Err(Error::from_response_body(status, &body, None))
}
}
async fn api_error_from_response(resp: Response, status: StatusCode) -> Error {
let retry_after = parse_retry_after(&resp);
let body = resp.bytes().await.unwrap_or_default();
Error::from_response_body(status.as_u16(), &body, retry_after)
}