Skip to content

Commit 32bf654

Browse files
authored
Merge pull request #31 from mrubyedge/fetch-headers
Support headers in fetch
2 parents 23bc2ed + d397834 commit 32bf654

9 files changed

Lines changed: 245 additions & 16 deletions

File tree

uzumibi-cli/templates/cloudflare/__features__/enable-external/src/index.js

Lines changed: 24 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -44,12 +44,13 @@ export default {
4444
return 0;
4545
},
4646

47-
// Fetch.fetch(url, method, body) -> packed Uzumibi::Response
47+
// Fetch.fetch(url, method, body, headers) -> packed Uzumibi::Response
4848
// Format: u16 status | u16 headers_count | (u16 key_size, key, u16 value_size, value)... | u32 body_size | body
4949
uzumibi_cf_fetch: async (
5050
urlPtr, urlSize,
5151
methodPtr, methodSize,
5252
bodyPtr, bodySize,
53+
headersPtr, headersSize,
5354
resultPtr, resultMaxSize
5455
) => {
5556
const memory = exports.memory;
@@ -64,6 +65,28 @@ export default {
6465
fetchOptions.body = body;
6566
}
6667

68+
// Unpack request headers: u16 LE count, then (u16 LE key_size, key, u16 LE value_size, value) * count
69+
if (headersSize >= 2) {
70+
const hView = new DataView(memory.buffer, headersPtr, headersSize);
71+
const hCount = hView.getUint16(0, true);
72+
if (hCount > 0) {
73+
const reqHeaders = {};
74+
let hPos = 2;
75+
for (let i = 0; i < hCount; i++) {
76+
const kLen = hView.getUint16(hPos, true);
77+
hPos += 2;
78+
const k = decoder.decode(new Uint8Array(memory.buffer, headersPtr + hPos, kLen));
79+
hPos += kLen;
80+
const vLen = hView.getUint16(hPos, true);
81+
hPos += 2;
82+
const v = decoder.decode(new Uint8Array(memory.buffer, headersPtr + hPos, vLen));
83+
hPos += vLen;
84+
reqHeaders[k] = v;
85+
}
86+
fetchOptions.headers = reqHeaders;
87+
}
88+
}
89+
6790
const response = await fetch(url, fetchOptions);
6891
const responseBody = await response.text();
6992

uzumibi-cli/templates/cloudflare/__features__/enable-external/wasm-app/src/lib.rs

Lines changed: 49 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -44,6 +44,8 @@ unsafe extern "C" {
4444
method_size: usize,
4545
body_ptr: *const u8,
4646
body_size: usize,
47+
headers_ptr: *const u8,
48+
headers_size: usize,
4749
result_ptr: *mut u8,
4850
result_max_size: usize,
4951
) -> i32;
@@ -82,7 +84,7 @@ fn debug_console_log_internal(message: &str) {
8284
/// u32 LE body_size
8385
/// body bytes
8486
#[cfg(feature = "enable-external")]
85-
fn cf_fetch(url: &str, method: &str, body: &str) -> Result<Vec<u8>, String> {
87+
fn cf_fetch(url: &str, method: &str, body: &str, headers: &[u8]) -> Result<Vec<u8>, String> {
8688
const BUFFER_SIZE: usize = 65536;
8789
let mut buffer = vec![0u8; BUFFER_SIZE];
8890

@@ -94,6 +96,8 @@ fn cf_fetch(url: &str, method: &str, body: &str) -> Result<Vec<u8>, String> {
9496
method.len(),
9597
body.as_ptr(),
9698
body.len(),
99+
headers.as_ptr(),
100+
headers.len(),
97101
buffer.as_mut_ptr(),
98102
BUFFER_SIZE,
99103
);
@@ -181,7 +185,7 @@ fn uzumibi_kernel_debug_console_log(
181185
Ok(RObject::nil().to_refcount_assigned())
182186
}
183187

184-
/// Fetch.fetch(url, method="GET", body="") -> Uzumibi::Response
188+
/// Fetch.fetch(url, method="GET", body="", headers={}) -> Uzumibi::Response
185189
#[cfg(feature = "enable-external")]
186190
fn uzumibi_fetch_class_fetch(
187191
vm: &mut VM,
@@ -207,13 +211,55 @@ fn uzumibi_fetch_class_fetch(
207211
String::new()
208212
};
209213

210-
let packed = cf_fetch(&url, &method, &body)
214+
// Pack request headers from Hash (4th argument)
215+
let packed_headers = if args.len() > 3 {
216+
pack_headers_from_hash(vm, &args[3])?
217+
} else {
218+
vec![0u8; 2] // u16 LE count = 0
219+
};
220+
221+
let packed = cf_fetch(&url, &method, &body, &packed_headers)
211222
.map_err(|e| mrubyedge::Error::RuntimeError(format!("Fetch failed: {}", e)))?;
212223

213224
// Unpack the packed response into Uzumibi::Response
214225
unpack_response_to_robject(vm, &packed)
215226
}
216227

228+
/// Pack a mruby Hash into binary format for request headers:
229+
/// u16 LE headers_count
230+
/// (u16 LE key_size, key bytes, u16 LE value_size, value bytes) * count
231+
#[cfg(feature = "enable-external")]
232+
fn pack_headers_from_hash(
233+
vm: &mut VM,
234+
hash_obj: &Rc<RObject>,
235+
) -> Result<Vec<u8>, mrubyedge::Error> {
236+
match &hash_obj.as_ref().value {
237+
RValue::Hash(h) => {
238+
let hash = h.borrow();
239+
let mut buf = Vec::new();
240+
let count = hash.len() as u16;
241+
buf.extend_from_slice(&count.to_le_bytes());
242+
for (_, (key_obj, value_obj)) in hash.iter() {
243+
let key = mrb_funcall(vm, key_obj.clone().into(), "to_s", &[])?;
244+
let key: String = key.as_ref().try_into()?;
245+
let value = mrb_funcall(vm, value_obj.clone().into(), "to_s", &[])?;
246+
let value: String = value.as_ref().try_into()?;
247+
buf.extend_from_slice(&(key.len() as u16).to_le_bytes());
248+
buf.extend_from_slice(key.as_bytes());
249+
buf.extend_from_slice(&(value.len() as u16).to_le_bytes());
250+
buf.extend_from_slice(value.as_bytes());
251+
}
252+
Ok(buf)
253+
}
254+
RValue::Nil => {
255+
Ok(vec![0u8; 2]) // u16 LE count = 0
256+
}
257+
_ => Err(mrubyedge::Error::RuntimeError(
258+
"headers argument must be a Hash".to_string(),
259+
)),
260+
}
261+
}
262+
217263
/// Unpack packed binary response into Uzumibi::Response mruby object
218264
#[cfg(feature = "enable-external")]
219265
fn unpack_response_to_robject(vm: &mut VM, buf: &[u8]) -> Result<Rc<RObject>, mrubyedge::Error> {

uzumibi-cli/templates/cloudflare/__features__/queue/src/index.js

Lines changed: 24 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -48,11 +48,12 @@ export default {
4848
return 0;
4949
},
5050

51-
// Fetch.fetch(url, method, body) -> packed Uzumibi::Response
51+
// Fetch.fetch(url, method, body, headers) -> packed Uzumibi::Response
5252
uzumibi_cf_fetch: async (
5353
urlPtr, urlSize,
5454
methodPtr, methodSize,
5555
bodyPtr, bodySize,
56+
headersPtr, headersSize,
5657
resultPtr, resultMaxSize,
5758
) => {
5859
const memory = exports.memory;
@@ -67,6 +68,28 @@ export default {
6768
fetchOptions.body = body;
6869
}
6970

71+
// Unpack request headers: u16 LE count, then (u16 LE key_size, key, u16 LE value_size, value) * count
72+
if (headersSize >= 2) {
73+
const hView = new DataView(memory.buffer, headersPtr, headersSize);
74+
const hCount = hView.getUint16(0, true);
75+
if (hCount > 0) {
76+
const reqHeaders = {};
77+
let hPos = 2;
78+
for (let i = 0; i < hCount; i++) {
79+
const kLen = hView.getUint16(hPos, true);
80+
hPos += 2;
81+
const k = decoder.decode(new Uint8Array(memory.buffer, headersPtr + hPos, kLen));
82+
hPos += kLen;
83+
const vLen = hView.getUint16(hPos, true);
84+
hPos += 2;
85+
const v = decoder.decode(new Uint8Array(memory.buffer, headersPtr + hPos, vLen));
86+
hPos += vLen;
87+
reqHeaders[k] = v;
88+
}
89+
fetchOptions.headers = reqHeaders;
90+
}
91+
}
92+
7093
const response = await fetch(url, fetchOptions);
7194
const responseBody = await response.text();
7295

uzumibi-cli/templates/cloudflare/__features__/queue/wasm-app/src/lib.rs

Lines changed: 49 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -57,6 +57,8 @@ unsafe extern "C" {
5757
method_size: usize,
5858
body_ptr: *const u8,
5959
body_size: usize,
60+
headers_ptr: *const u8,
61+
headers_size: usize,
6062
result_ptr: *mut u8,
6163
result_max_size: usize,
6264
) -> i32;
@@ -95,7 +97,7 @@ fn debug_console_log_internal(message: &str) {
9597
/// u32 LE body_size
9698
/// body bytes
9799
#[cfg(feature = "enable-external")]
98-
fn cf_fetch(url: &str, method: &str, body: &str) -> Result<Vec<u8>, String> {
100+
fn cf_fetch(url: &str, method: &str, body: &str, headers: &[u8]) -> Result<Vec<u8>, String> {
99101
const BUFFER_SIZE: usize = 65536;
100102
let mut buffer = vec![0u8; BUFFER_SIZE];
101103

@@ -107,6 +109,8 @@ fn cf_fetch(url: &str, method: &str, body: &str) -> Result<Vec<u8>, String> {
107109
method.len(),
108110
body.as_ptr(),
109111
body.len(),
112+
headers.as_ptr(),
113+
headers.len(),
110114
buffer.as_mut_ptr(),
111115
BUFFER_SIZE,
112116
);
@@ -194,7 +198,7 @@ fn uzumibi_kernel_debug_console_log(
194198
Ok(RObject::nil().to_refcount_assigned())
195199
}
196200

197-
/// Fetch.fetch(url, method="GET", body="") -> Uzumibi::Response
201+
/// Fetch.fetch(url, method="GET", body="", headers={}) -> Uzumibi::Response
198202
#[cfg(feature = "enable-external")]
199203
fn uzumibi_fetch_class_fetch(
200204
vm: &mut VM,
@@ -220,13 +224,55 @@ fn uzumibi_fetch_class_fetch(
220224
String::new()
221225
};
222226

223-
let packed = cf_fetch(&url, &method, &body)
227+
// Pack request headers from Hash (4th argument)
228+
let packed_headers = if args.len() > 3 {
229+
pack_headers_from_hash(vm, &args[3])?
230+
} else {
231+
vec![0u8; 2] // u16 LE count = 0
232+
};
233+
234+
let packed = cf_fetch(&url, &method, &body, &packed_headers)
224235
.map_err(|e| mrubyedge::Error::RuntimeError(format!("Fetch failed: {}", e)))?;
225236

226237
// Unpack the packed response into Uzumibi::Response
227238
unpack_response_to_robject(vm, &packed)
228239
}
229240

241+
/// Pack a mruby Hash into binary format for request headers:
242+
/// u16 LE headers_count
243+
/// (u16 LE key_size, key bytes, u16 LE value_size, value bytes) * count
244+
#[cfg(feature = "enable-external")]
245+
fn pack_headers_from_hash(
246+
vm: &mut VM,
247+
hash_obj: &Rc<RObject>,
248+
) -> Result<Vec<u8>, mrubyedge::Error> {
249+
match &hash_obj.as_ref().value {
250+
RValue::Hash(h) => {
251+
let hash = h.borrow();
252+
let mut buf = Vec::new();
253+
let count = hash.len() as u16;
254+
buf.extend_from_slice(&count.to_le_bytes());
255+
for (_, (key_obj, value_obj)) in hash.iter() {
256+
let key = mrb_funcall(vm, key_obj.clone().into(), "to_s", &[])?;
257+
let key: String = key.as_ref().try_into()?;
258+
let value = mrb_funcall(vm, value_obj.clone().into(), "to_s", &[])?;
259+
let value: String = value.as_ref().try_into()?;
260+
buf.extend_from_slice(&(key.len() as u16).to_le_bytes());
261+
buf.extend_from_slice(key.as_bytes());
262+
buf.extend_from_slice(&(value.len() as u16).to_le_bytes());
263+
buf.extend_from_slice(value.as_bytes());
264+
}
265+
Ok(buf)
266+
}
267+
RValue::Nil => {
268+
Ok(vec![0u8; 2]) // u16 LE count = 0
269+
}
270+
_ => Err(mrubyedge::Error::RuntimeError(
271+
"headers argument must be a Hash".to_string(),
272+
)),
273+
}
274+
}
275+
230276
/// Unpack packed binary response into Uzumibi::Response mruby object
231277
#[cfg(feature = "enable-external")]
232278
fn unpack_response_to_robject(vm: &mut VM, buf: &[u8]) -> Result<Rc<RObject>, mrubyedge::Error> {

uzumibi-cli/tests/runn/help.yml

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,6 @@
11
desc: Test uzumibi CLI help command
22
vars:
33
binary: ${UZUMIBI_TEST_BINARY:-../target/release/uzumibi}
4-
version: 0.6.0-rc2
54
steps:
65
build:
76
desc: Build release binary
@@ -28,7 +27,7 @@ steps:
2827
version:
2928
desc: Show version
3029
exec:
31-
command: "{{ vars.binary }} --version | grep {{ vars.version }}"
30+
command: "{{ vars.binary }} --version"
3231
test: |
3332
current.exit_code == 0 &&
3433
current.stdout contains "uzumibi"

uzumibi-on-cloudflare-spike/src/index.js

Lines changed: 24 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -44,12 +44,13 @@ export default {
4444
return 0;
4545
},
4646

47-
// Fetch.fetch(url, method, body) -> packed Uzumibi::Response
47+
// Fetch.fetch(url, method, body, headers) -> packed Uzumibi::Response
4848
// Format: u16 status | u16 headers_count | (u16 key_size, key, u16 value_size, value)... | u32 body_size | body
4949
uzumibi_cf_fetch: async (
5050
urlPtr, urlSize,
5151
methodPtr, methodSize,
5252
bodyPtr, bodySize,
53+
headersPtr, headersSize,
5354
resultPtr, resultMaxSize
5455
) => {
5556
const memory = exports.memory;
@@ -64,6 +65,28 @@ export default {
6465
fetchOptions.body = body;
6566
}
6667

68+
// Unpack request headers: u16 LE count, then (u16 LE key_size, key, u16 LE value_size, value) * count
69+
if (headersSize >= 2) {
70+
const hView = new DataView(memory.buffer, headersPtr, headersSize);
71+
const hCount = hView.getUint16(0, true);
72+
if (hCount > 0) {
73+
const reqHeaders = {};
74+
let hPos = 2;
75+
for (let i = 0; i < hCount; i++) {
76+
const kLen = hView.getUint16(hPos, true);
77+
hPos += 2;
78+
const k = decoder.decode(new Uint8Array(memory.buffer, headersPtr + hPos, kLen));
79+
hPos += kLen;
80+
const vLen = hView.getUint16(hPos, true);
81+
hPos += 2;
82+
const v = decoder.decode(new Uint8Array(memory.buffer, headersPtr + hPos, vLen));
83+
hPos += vLen;
84+
reqHeaders[k] = v;
85+
}
86+
fetchOptions.headers = reqHeaders;
87+
}
88+
}
89+
6790
const response = await fetch(url, fetchOptions);
6891
const responseBody = await response.text();
6992

uzumibi-on-cloudflare-spike/src/index.queue.js

Lines changed: 24 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -48,11 +48,12 @@ export default {
4848
return 0;
4949
},
5050

51-
// Fetch.fetch(url, method, body) -> packed Uzumibi::Response
51+
// Fetch.fetch(url, method, body, headers) -> packed Uzumibi::Response
5252
uzumibi_cf_fetch: async (
5353
urlPtr, urlSize,
5454
methodPtr, methodSize,
5555
bodyPtr, bodySize,
56+
headersPtr, headersSize,
5657
resultPtr, resultMaxSize,
5758
) => {
5859
const memory = exports.memory;
@@ -67,6 +68,28 @@ export default {
6768
fetchOptions.body = body;
6869
}
6970

71+
// Unpack request headers: u16 LE count, then (u16 LE key_size, key, u16 LE value_size, value) * count
72+
if (headersSize >= 2) {
73+
const hView = new DataView(memory.buffer, headersPtr, headersSize);
74+
const hCount = hView.getUint16(0, true);
75+
if (hCount > 0) {
76+
const reqHeaders = {};
77+
let hPos = 2;
78+
for (let i = 0; i < hCount; i++) {
79+
const kLen = hView.getUint16(hPos, true);
80+
hPos += 2;
81+
const k = decoder.decode(new Uint8Array(memory.buffer, headersPtr + hPos, kLen));
82+
hPos += kLen;
83+
const vLen = hView.getUint16(hPos, true);
84+
hPos += 2;
85+
const v = decoder.decode(new Uint8Array(memory.buffer, headersPtr + hPos, vLen));
86+
hPos += vLen;
87+
reqHeaders[k] = v;
88+
}
89+
fetchOptions.headers = reqHeaders;
90+
}
91+
}
92+
7093
const response = await fetch(url, fetchOptions);
7194
const responseBody = await response.text();
7295

uzumibi-on-cloudflare-spike/wasm-app/Cargo.toml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,6 @@ uzumibi-art-router = ">= 0.3.1"
1717
mruby-compiler2-sys = ">= 0.3.0"
1818

1919
[features]
20-
default = []
20+
default = ["enable-external"]
2121
enable-external = []
2222
queue = ["enable-external"]

0 commit comments

Comments
 (0)