rp_supabase_functions/
lib.rs1#![cfg_attr(doc, doc = include_str!("../README.md"))]
2mod error;
3
4pub use error::FunctionsError;
5pub use reqwest::Method;
6use reqwest::header::{AUTHORIZATION, CONTENT_TYPE, HeaderMap, HeaderName, HeaderValue};
7use serde::Serialize;
8use serde::de::DeserializeOwned;
9pub use url;
10use url::Url;
11
12const RELAY_ERROR_HEADER: &str = "x-relay-error";
13const REGION_HEADER: &str = "x-region";
14
15#[derive(Debug, Clone)]
19pub struct FunctionsClient {
20 http: reqwest::Client,
21 base: Url,
22 headers: HeaderMap,
23}
24
25impl FunctionsClient {
26 pub fn new(project_url: &Url, api_key: &str) -> Result<Self, FunctionsError> {
39 Self::new_with_client(project_url, api_key, default_client()?)
40 }
41
42 pub fn new_with_client(
48 project_url: &Url,
49 api_key: &str,
50 http: reqwest::Client,
51 ) -> Result<Self, FunctionsError> {
52 if project_url.cannot_be_a_base() {
53 return Err(FunctionsError::UrlNotBase);
54 }
55 let mut root = project_url.clone();
56 if !root.path().ends_with('/') {
57 let path = format!("{}/", root.path());
58 root.set_path(&path);
59 }
60 let base = root.join("functions/v1/")?;
61 let mut headers = HeaderMap::new();
62 headers.insert("apikey", sensitive(api_key)?);
63 if !is_new_format_key(api_key) {
64 headers.insert(AUTHORIZATION, sensitive(&format!("Bearer {api_key}"))?);
65 }
66 Ok(Self {
67 http,
68 base,
69 headers,
70 })
71 }
72
73 pub fn with_access_token(&self, token: &str) -> Result<Self, FunctionsError> {
81 let mut next = self.clone();
82 next.headers
83 .insert(AUTHORIZATION, sensitive(&format!("Bearer {token}"))?);
84 Ok(next)
85 }
86
87 pub fn invoke(&self, name: &str) -> InvokeBuilder {
93 let url = if is_valid_function_name(name) {
94 let mut url = self.base.clone();
95 url.path_segments_mut()
96 .map_err(|()| FunctionsError::UrlNotBase)
97 .map(|mut segments| {
98 segments.pop_if_empty().push(name);
99 })
100 .map(|()| url)
101 } else {
102 Err(FunctionsError::InvalidFunctionName(name.to_owned()))
103 };
104 InvokeBuilder {
105 http: self.http.clone(),
106 state: url.map(|url| Request {
107 url,
108 method: Method::POST,
109 headers: self.headers.clone(),
110 body: None,
111 }),
112 }
113 }
114}
115
116fn default_client() -> Result<reqwest::Client, reqwest::Error> {
117 reqwest::Client::builder()
118 .redirect(reqwest::redirect::Policy::custom(|attempt| {
119 let same_origin = attempt
120 .previous()
121 .last()
122 .is_some_and(|prev| prev.origin() == attempt.url().origin());
123 if !same_origin {
124 attempt.stop()
125 } else if attempt.previous().len() > 10 {
126 attempt.error("too many redirects")
127 } else {
128 attempt.follow()
129 }
130 }))
131 .build()
132}
133
134fn is_new_format_key(api_key: &str) -> bool {
135 api_key.starts_with("sb_publishable_") || api_key.starts_with("sb_secret_")
136}
137
138fn is_valid_function_name(name: &str) -> bool {
139 !matches!(name, "" | "." | "..") && !name.contains(['/', '\t', '\r', '\n'])
140}
141
142fn sensitive(value: &str) -> Result<HeaderValue, FunctionsError> {
143 let mut header = HeaderValue::from_str(value)?;
144 header.set_sensitive(true);
145 Ok(header)
146}
147
148#[derive(Debug)]
149struct Request {
150 url: Url,
151 method: Method,
152 headers: HeaderMap,
153 body: Option<Vec<u8>>,
154}
155
156#[derive(Debug)]
160#[must_use = "call `send`, `fetch`, or `text` to run the request"]
161pub struct InvokeBuilder {
162 http: reqwest::Client,
163 state: Result<Request, FunctionsError>,
164}
165
166impl InvokeBuilder {
167 fn update(mut self, apply: impl FnOnce(&mut Request) -> Result<(), FunctionsError>) -> Self {
168 if let Ok(request) = self.state.as_mut() {
169 if let Err(error) = apply(request) {
170 self.state = Err(error);
171 }
172 }
173 self
174 }
175
176 pub fn method(self, method: Method) -> Self {
178 self.update(|request| {
179 request.method = method;
180 Ok(())
181 })
182 }
183
184 pub fn json<T: Serialize + ?Sized>(self, value: &T) -> Self {
186 self.update(|request| {
187 let bytes = serde_json::to_vec(value).map_err(FunctionsError::Serialize)?;
188 request
189 .headers
190 .insert(CONTENT_TYPE, HeaderValue::from_static("application/json"));
191 request.body = Some(bytes);
192 Ok(())
193 })
194 }
195
196 pub fn body<B: Into<Vec<u8>>>(self, bytes: B, content_type: &str) -> Self {
198 self.update(|request| {
199 request
200 .headers
201 .insert(CONTENT_TYPE, HeaderValue::from_str(content_type)?);
202 request.body = Some(bytes.into());
203 Ok(())
204 })
205 }
206
207 pub fn header(self, name: &str, value: &str) -> Self {
209 self.update(|request| {
210 let name = HeaderName::from_bytes(name.as_bytes())?;
211 let mut value = HeaderValue::from_str(value)?;
212 if name == AUTHORIZATION || name.as_str() == "apikey" {
213 value.set_sensitive(true);
214 }
215 request.headers.insert(name, value);
216 Ok(())
217 })
218 }
219
220 pub fn region(self, region: &str) -> Self {
222 self.header(REGION_HEADER, region)
223 }
224
225 pub async fn send(self) -> Result<reqwest::Response, FunctionsError> {
233 let request = self.state?;
234 let mut builder = self
235 .http
236 .request(request.method, request.url)
237 .headers(request.headers);
238 if let Some(body) = request.body {
239 builder = builder.body(body);
240 }
241 let response = builder.send().await?;
242 let relay = response
243 .headers()
244 .get(RELAY_ERROR_HEADER)
245 .is_some_and(|value| value.as_bytes() == b"true");
246 let status = response.status();
247 if relay {
248 let body = response.text().await?;
249 return Err(FunctionsError::Relay { status, body });
250 }
251 if !status.is_success() {
252 let body = response.text().await?;
253 return Err(FunctionsError::Http { status, body });
254 }
255 Ok(response)
256 }
257
258 pub async fn fetch<T: DeserializeOwned>(self) -> Result<T, FunctionsError> {
264 let bytes = self.send().await?.bytes().await?;
265 serde_json::from_slice(&bytes).map_err(FunctionsError::Decode)
266 }
267
268 pub async fn text(self) -> Result<String, FunctionsError> {
274 Ok(self.send().await?.text().await?)
275 }
276}