Implement automatic decompression of net responses

This commit is contained in:
Filip Tibell 2023-05-20 13:25:14 +02:00
parent b42c69f9c4
commit 6628220429
No known key found for this signature in database
5 changed files with 104 additions and 10 deletions

View file

@ -28,6 +28,9 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
assert(decompressed == INPUT) assert(decompressed == INPUT)
``` ```
- Added automatic decompression for compressed responses when using `net.request`.
This behavior can be disabled by passing `options = { decompress = false }` in request params.
- Added several new instance methods in the `roblox` builtin library: - Added several new instance methods in the `roblox` builtin library:
- [`Instance:AddTag`](https://create.roblox.com/docs/reference/engine/classes/Instance#AddTag) - [`Instance:AddTag`](https://create.roblox.com/docs/reference/engine/classes/Instance#AddTag)
- [`Instance:GetTags`](https://create.roblox.com/docs/reference/engine/classes/Instance#GetTags) - [`Instance:GetTags`](https://create.roblox.com/docs/reference/engine/classes/Instance#GetTags)

View file

@ -1,5 +1,19 @@
export type HttpMethod = "GET" | "POST" | "PUT" | "DELETE" | "HEAD" | "OPTIONS" | "PATCH" export type HttpMethod = "GET" | "POST" | "PUT" | "DELETE" | "HEAD" | "OPTIONS" | "PATCH"
--[=[
@type FetchParamsOptions
@within Net
Extra options for `FetchParams`.
This is a dictionary that may contain one or more of the following values:
* `decompress` - If the request body should be automatically decompressed when possible. Defaults to `true`
]=]
export type FetchParamsOptions = {
decompress: boolean?,
}
--[=[ --[=[
@type FetchParams @type FetchParams
@within Net @within Net
@ -10,16 +24,18 @@ export type HttpMethod = "GET" | "POST" | "PUT" | "DELETE" | "HEAD" | "OPTIONS"
* `url` - The URL to send a request to. This is always required * `url` - The URL to send a request to. This is always required
* `method` - The HTTP method verb, such as `"GET"`, `"POST"`, `"PATCH"`, `"PUT"`, or `"DELETE"`. Defaults to `"GET"` * `method` - The HTTP method verb, such as `"GET"`, `"POST"`, `"PATCH"`, `"PUT"`, or `"DELETE"`. Defaults to `"GET"`
* `body` - The request body
* `query` - A table of key-value pairs representing query parameters in the request path * `query` - A table of key-value pairs representing query parameters in the request path
* `headers` - A table of key-value pairs representing headers * `headers` - A table of key-value pairs representing headers
* `body` - The request body * `options` - Extra options for things such as automatic decompression of response bodies
]=] ]=]
export type FetchParams = { export type FetchParams = {
url: string, url: string,
method: HttpMethod?, method: HttpMethod?,
body: string?,
query: { [string]: string }?, query: { [string]: string }?,
headers: { [string]: string }?, headers: { [string]: string }?,
body: string?, options: FetchParamsOptions?,
} }
--[=[ --[=[

View file

@ -3,7 +3,10 @@ use std::collections::HashMap;
use mlua::prelude::*; use mlua::prelude::*;
use console::style; use console::style;
use hyper::Server; use hyper::{
header::{CONTENT_ENCODING, CONTENT_LENGTH},
Server,
};
use tokio::{sync::mpsc, task}; use tokio::{sync::mpsc, task};
use crate::lua::{ use crate::lua::{
@ -11,7 +14,7 @@ use crate::lua::{
NetClient, NetClientBuilder, NetLocalExec, NetService, NetWebSocket, RequestConfig, NetClient, NetClientBuilder, NetLocalExec, NetService, NetWebSocket, RequestConfig,
ServeConfig, ServeConfig,
}, },
serde::{EncodeDecodeConfig, EncodeDecodeFormat}, serde::{decompress, CompressDecompressFormat, EncodeDecodeConfig, EncodeDecodeFormat},
table::TableBuilder, table::TableBuilder,
task::{TaskScheduler, TaskSchedulerAsyncExt}, task::{TaskScheduler, TaskSchedulerAsyncExt},
}; };
@ -74,13 +77,38 @@ async fn net_request<'a>(lua: &'static Lua, config: RequestConfig<'a>) -> LuaRes
// Extract status, headers // Extract status, headers
let res_status = res.status().as_u16(); let res_status = res.status().as_u16();
let res_status_text = res.status().canonical_reason(); let res_status_text = res.status().canonical_reason();
let res_headers = res let mut res_headers = res
.headers() .headers()
.iter() .iter()
.map(|(name, value)| (name.to_string(), value.to_str().unwrap().to_owned())) .map(|(name, value)| {
(
name.as_str().to_string(),
value.to_str().unwrap().to_owned(),
)
})
.collect::<HashMap<String, String>>(); .collect::<HashMap<String, String>>();
// Read response bytes // Read response bytes
let res_bytes = res.bytes().await.map_err(LuaError::external)?; let mut res_bytes = res.bytes().await.map_err(LuaError::external)?.to_vec();
// Check for extra options, decompression
if config.options.decompress {
// NOTE: Header names are guaranteed to be lowercase because of the above
// transformations of them into the hashmap, so we can compare directly
let format = res_headers.iter().find_map(|(name, val)| {
if name == CONTENT_ENCODING.as_str() {
CompressDecompressFormat::detect_from_header_str(val)
} else {
None
}
});
if let Some(format) = format {
res_bytes = decompress(format, res_bytes).await?;
let content_encoding_header_str = CONTENT_ENCODING.as_str();
let content_length_header_str = CONTENT_LENGTH.as_str();
res_headers.retain(|name, _| {
name != content_encoding_header_str && name != content_length_header_str
});
}
}
// Construct and return a readonly lua table with results // Construct and return a readonly lua table with results
TableBuilder::new(lua)? TableBuilder::new(lua)?
.with_value("ok", (200..300).contains(&res_status))? .with_value("ok", (200..300).contains(&res_status))?

View file

@ -6,16 +6,56 @@ use reqwest::Method;
// Net request config // Net request config
#[derive(Debug, Clone)]
pub struct RequestConfigOptions {
pub decompress: bool,
}
impl Default for RequestConfigOptions {
fn default() -> Self {
Self { decompress: true }
}
}
impl<'lua> FromLua<'lua> for RequestConfigOptions {
fn from_lua(value: LuaValue<'lua>, _: &'lua Lua) -> LuaResult<Self> {
// Nil means default options, table means custom options
if let LuaValue::Nil = value {
return Ok(Self::default());
} else if let LuaValue::Table(tab) = value {
// Extract flags
let decompress = match tab.raw_get::<_, Option<bool>>("decompress") {
Ok(decomp) => Ok(decomp.unwrap_or(true)),
Err(_) => Err(LuaError::RuntimeError(
"Invalid option value for 'decompress' in request config options".to_string(),
)),
}?;
return Ok(Self { decompress });
}
// Anything else is invalid
Err(LuaError::FromLuaConversionError {
from: value.type_name(),
to: "RequestConfigOptions",
message: Some(format!(
"Invalid request config options - expected table or nil, got {}",
value.type_name()
)),
})
}
}
#[derive(Debug, Clone)]
pub struct RequestConfig<'a> { pub struct RequestConfig<'a> {
pub url: String, pub url: String,
pub method: Method, pub method: Method,
pub query: HashMap<LuaString<'a>, LuaString<'a>>, pub query: HashMap<LuaString<'a>, LuaString<'a>>,
pub headers: HashMap<LuaString<'a>, LuaString<'a>>, pub headers: HashMap<LuaString<'a>, LuaString<'a>>,
pub body: Option<Vec<u8>>, pub body: Option<Vec<u8>>,
pub options: RequestConfigOptions,
} }
impl<'lua> FromLua<'lua> for RequestConfig<'lua> { impl<'lua> FromLua<'lua> for RequestConfig<'lua> {
fn from_lua(value: LuaValue<'lua>, _: &'lua Lua) -> LuaResult<Self> { fn from_lua(value: LuaValue<'lua>, lua: &'lua Lua) -> LuaResult<Self> {
// If we just got a string we assume its a GET request to a given url // If we just got a string we assume its a GET request to a given url
if let LuaValue::String(s) = value { if let LuaValue::String(s) = value {
return Ok(Self { return Ok(Self {
@ -24,6 +64,7 @@ impl<'lua> FromLua<'lua> for RequestConfig<'lua> {
query: HashMap::new(), query: HashMap::new(),
headers: HashMap::new(), headers: HashMap::new(),
body: None, body: None,
options: Default::default(),
}); });
} }
// If we got a table we are able to configure the entire request // If we got a table we are able to configure the entire request
@ -84,6 +125,11 @@ impl<'lua> FromLua<'lua> for RequestConfig<'lua> {
&method &method
))), ))),
}?; }?;
// Parse any extra options given
let options = match tab.raw_get::<_, LuaValue>("options") {
Ok(opts) => RequestConfigOptions::from_lua(opts, lua)?,
Err(_) => RequestConfigOptions::default(),
};
// All good, validated and we got what we need // All good, validated and we got what we need
return Ok(Self { return Ok(Self {
url, url,
@ -91,6 +137,7 @@ impl<'lua> FromLua<'lua> for RequestConfig<'lua> {
query, query,
headers, headers,
body, body,
options,
}); });
}; };
// Anything else is invalid // Anything else is invalid

View file

@ -54,9 +54,9 @@ impl CompressDecompressFormat {
pub fn detect_from_header_str(header: impl AsRef<str>) -> Option<Self> { pub fn detect_from_header_str(header: impl AsRef<str>) -> Option<Self> {
// https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Content-Encoding#directives // https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Content-Encoding#directives
match header.as_ref().to_ascii_lowercase().trim() { match header.as_ref().to_ascii_lowercase().trim() {
"br" => Some(Self::Brotli), "br" | "brotli" => Some(Self::Brotli),
"deflate" => Some(Self::ZLib), "deflate" => Some(Self::ZLib),
"gzip" => Some(Self::GZip), "gz" | "gzip" => Some(Self::GZip),
_ => None, _ => None,
} }
} }