Skip to content

Commit db536ba

Browse files
committed
feat: Add encoding parameter to fetch function
- Adds a new parameter `response_encoding` to the fetch function. - This parameter allows the user to specify a charset for decoding the response body. - It supports all standard web encodings, and also `hex` and `base64`. - If no encoding is specified, the response is decoded as utf-8, or as base64 if it contains binary data.
1 parent 6e4e936 commit db536ba

File tree

8 files changed

+78
-26
lines changed

8 files changed

+78
-26
lines changed

CHANGELOG.md

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,11 @@
11
# CHANGELOG.md
22

3+
## v0.37.0
4+
- Added a new parameter `encoding` to the [fetch](https://sql-page.com/functions.sql?function=fetch) function:
5+
- All [standard web encodings](https://encoding.spec.whatwg.org/#concept-encoding-get) are supported.
6+
- Additionally, `base64` can be specified to decode binary data as base64 (compatible with [data URI](https://developer.mozilla.org/en-US/docs/Web/HTTP/Basics_of_HTTP/Data_URIs))
7+
- By default, the old behavior of the `fetch_with_meta` function is preserved: the response body is decoded as `utf-8` if possible, otherwise the response is encoded in `base64`.
8+
39
## v0.36.1
410
- Fix regression introduced in v0.36.0: PostgreSQL money values showed as 0.0
511
- The recommended way to display money values in postgres is still to format them in the way you expect in SQL. See https://github.com/sqlpage/SQLPage/issues/983

Cargo.lock

Lines changed: 2 additions & 1 deletion
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

Cargo.toml

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
[package]
22
name = "sqlpage"
3-
version = "0.36.1"
3+
version = "0.37.0"
44
edition = "2021"
55
description = "Build data user interfaces entirely in SQL. A web server that takes .sql files and formats the query result using pre-made configurable professional-looking components."
66
keywords = ["web", "sql", "framework"]
@@ -73,6 +73,7 @@ awc = { version = "3", features = ["rustls-0_23-webpki-roots"] }
7373
clap = { version = "4.5.17", features = ["derive"] }
7474
tokio-util = "0.7.12"
7575
openidconnect = { version = "4.0.0", default-features = false }
76+
encoding_rs = "0.8.35"
7677

7778
[build-dependencies]
7879
awc = { version = "3", features = ["rustls-0_23-webpki-roots"] }

examples/official-site/sqlpage/migrations/40_fetch.sql

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -87,6 +87,7 @@ The fetch function accepts either a URL string, or a JSON object with the follow
8787
- `timeout_ms`: The maximum time to wait for the request, in milliseconds. Defaults to 5000.
8888
- `username`: Optional username for HTTP Basic Authentication. Introduced in version 0.33.0.
8989
- `password`: Optional password for HTTP Basic Authentication. Only used if username is provided. Introduced in version 0.33.0.
90+
- `response_encoding`: Optional charset to use for decoding the response body. Defaults to `utf8`, or `base64` if the response contains binary data. All [standard web encodings](https://encoding.spec.whatwg.org/#concept-encoding-get) are supported, plus `hex`, `base64`, and `base64url`. Introduced in version 0.37.0.
9091
9192
# Error handling and reading response headers
9293

src/webserver/database/sqlpage_functions/functions.rs

Lines changed: 46 additions & 24 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@ use crate::webserver::{
1111
use anyhow::{anyhow, Context};
1212
use futures_util::StreamExt;
1313
use mime_guess::mime;
14+
use std::fmt::Write;
1415
use std::{borrow::Cow, ffi::OsStr, str::FromStr};
1516

1617
super::function_definition_macro::sqlpage_functions! {
@@ -213,13 +214,46 @@ async fn fetch(
213214
)
214215
})?
215216
.to_vec();
216-
let response_str = String::from_utf8(body).with_context(
217-
|| format!("Unable to convert the response from {} to a string. Only UTF-8 responses are supported.", http_request.url),
218-
)?;
217+
let response_str = decode_response(body, http_request.response_encoding.as_deref())?;
219218
log::debug!("Fetch response: {response_str}");
220219
Ok(response_str)
221220
}
222221

222+
fn decode_response(response: Vec<u8>, encoding: Option<&str>) -> anyhow::Result<String> {
223+
match encoding {
224+
Some("base64") => Ok(base64::Engine::encode(
225+
&base64::engine::general_purpose::STANDARD,
226+
response,
227+
)),
228+
Some("base64url") => Ok(base64::Engine::encode(
229+
&base64::engine::general_purpose::URL_SAFE,
230+
response,
231+
)),
232+
Some("hex") => Ok(response.into_iter().fold(String::new(), |mut acc, byte| {
233+
write!(&mut acc, "{byte:02x}").unwrap();
234+
acc
235+
})),
236+
Some(encoding_label) => Ok(encoding_rs::Encoding::for_label(encoding_label.as_bytes())
237+
.with_context(|| format!("Invalid encoding name: {encoding_label}"))?
238+
.decode(&response)
239+
.0
240+
.into_owned()),
241+
None => {
242+
let body_str = String::from_utf8(response);
243+
match body_str {
244+
Ok(body_str) => Ok(body_str),
245+
Err(decoding_error) => {
246+
log::warn!("fetch(...) response is not UTF-8 and no encoding was specified. Decoding the response as base64. Please explicitly set the encoding to \"base64\" if this is the expected behavior.");
247+
Ok(base64::Engine::encode(
248+
&base64::engine::general_purpose::STANDARD,
249+
decoding_error.into_bytes(),
250+
))
251+
}
252+
}
253+
}
254+
}
255+
}
256+
223257
async fn fetch_with_meta(
224258
request: &RequestInfo,
225259
http_request: super::http_fetch_request::HttpFetchRequest<'_>,
@@ -270,27 +304,15 @@ async fn fetch_with_meta(
270304
match response.body().await {
271305
Ok(body) => {
272306
let body_bytes = body.to_vec();
273-
let body_str = String::from_utf8(body_bytes);
274-
275-
match body_str {
276-
Ok(body_str) if is_json => {
277-
obj.serialize_entry(
278-
"body",
279-
&serde_json::value::RawValue::from_string(body_str)?,
280-
)?;
281-
}
282-
Ok(body_str) => {
283-
obj.serialize_entry("body", &body_str)?;
284-
}
285-
Err(utf8_err) => {
286-
let mut base64_string = String::new();
287-
base64::Engine::encode_string(
288-
&base64::engine::general_purpose::STANDARD,
289-
utf8_err.as_bytes(),
290-
&mut base64_string,
291-
);
292-
obj.serialize_entry("body", &base64_string)?;
293-
}
307+
let body_str =
308+
decode_response(body_bytes, http_request.response_encoding.as_deref())?;
309+
if is_json {
310+
obj.serialize_entry(
311+
"json_body",
312+
&serde_json::value::RawValue::from_string(body_str)?,
313+
)?;
314+
} else {
315+
obj.serialize_entry("body", &body_str)?;
294316
}
295317
}
296318
Err(e) => {

src/webserver/database/sqlpage_functions/http_fetch_request.rs

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -38,6 +38,7 @@ pub(super) struct HttpFetchRequest<'b> {
3838
#[serde(borrow)]
3939
pub body: Option<Cow<'b, serde_json::value::RawValue>>,
4040
pub timeout_ms: Option<u64>,
41+
pub response_encoding: Option<Cow<'b, str>>,
4142
}
4243

4344
fn deserialize_map_to_vec_pairs<'de, D: serde::Deserializer<'de>>(
@@ -78,6 +79,7 @@ impl<'a> BorrowFromStr<'a> for HttpFetchRequest<'a> {
7879
password: None,
7980
body: None,
8081
timeout_ms: None,
82+
response_encoding: None,
8183
}
8284
} else {
8385
match s {
@@ -104,6 +106,7 @@ impl HttpFetchRequest<'_> {
104106
timeout_ms: self.timeout_ms,
105107
username: self.username.map(Cow::into_owned).map(Cow::Owned),
106108
password: self.password.map(Cow::into_owned).map(Cow::Owned),
109+
response_encoding: self.response_encoding.map(Cow::into_owned).map(Cow::Owned),
107110
}
108111
}
109112
}
Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,9 @@
1+
set res = sqlpage.fetch('{
2+
"url": "http://localhost:' || $echo_port || '/hello_world",
3+
"response_encoding": "base64"
4+
}');
5+
select 'text' as component,
6+
case
7+
when $res LIKE 'R0VUIC9oZWxsb193b3Js%' then 'It works !'
8+
else 'It failed ! Got: ' || $res
9+
end as contents;
Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,9 @@
1+
set res = sqlpage.fetch('{
2+
"url": "http://localhost:' || $echo_port || '/hello_world",
3+
"response_encoding": "hex"
4+
}');
5+
select 'text' as component,
6+
case
7+
when $res LIKE '474554202f68656c6c6f5f776f726c64%' then 'It works !'
8+
else 'It failed ! Got: ' || $res
9+
end as contents;

0 commit comments

Comments
 (0)