Skip to content

Commit 4d2f04e

Browse files
goffrieConvex, Inc.
authored andcommitted
Always use "isomorphic encoding" to convert HTTP headers <-> JS strings (#42536)
The Fetch Standard says that HTTP headers are WebIDL ByteStrings which use this encoding. GitOrigin-RevId: 87b690660c982e5e54e74918277456963f2d8890
1 parent 7bd0d42 commit 4d2f04e

File tree

2 files changed

+26
-8
lines changed

2 files changed

+26
-8
lines changed

crates/isolate/src/http.rs

Lines changed: 24 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,7 @@ use headers::{
1818
HeaderName,
1919
};
2020
use http::{
21+
HeaderValue,
2122
Method,
2223
StatusCode,
2324
};
@@ -50,7 +51,7 @@ impl HttpRequestV8 {
5051
) -> anyhow::Result<HttpRequestStream> {
5152
let mut header_map = HeaderMap::new();
5253
for (name, value) in &self.header_pairs {
53-
header_map.append(HeaderName::from_str(name.as_str())?, value.parse()?);
54+
header_map.append(HeaderName::from_str(name)?, byte_string_to_header(value)?);
5455
}
5556
let (body_sender, body_receiver) = spsc::unbounded_channel();
5657
match self.stream_id {
@@ -86,9 +87,9 @@ impl HttpRequestV8 {
8687
// None as the HeaderName for headers with multiple values
8788
// (https://docs.rs/http/latest/http/header/struct.HeaderMap.html#method.into_iter)
8889
for (name, value) in &request.headers {
89-
let value_str = value.to_str()?;
90+
let value_str = header_to_byte_string(value);
9091
let header_name_str = name.as_str();
91-
header_pairs.push((header_name_str.to_string(), value_str.to_string()));
92+
header_pairs.push((header_name_str.to_string(), value_str));
9293
}
9394

9495
Ok(Self {
@@ -117,7 +118,10 @@ impl HttpResponseV8 {
117118

118119
let mut header_map = HeaderMap::new();
119120
for (name, value) in &self.header_pairs {
120-
header_map.append(HeaderName::from_str(name.as_str())?, value.parse()?);
121+
header_map.append(
122+
HeaderName::from_str(name.as_str())?,
123+
byte_string_to_header(value)?,
124+
);
121125
}
122126

123127
Ok((
@@ -144,9 +148,7 @@ impl HttpResponseV8 {
144148
// None as the HeaderName for headers with multiple values
145149
// (https://docs.rs/http/latest/http/header/struct.HeaderMap.html#method.into_iter)
146150
for (name, value) in &response.headers {
147-
// Don't use `value.to_str()` since that does not support non-ASCII headers
148-
let value_bytes = value.as_bytes();
149-
let value_str: String = value_bytes.iter().map(|&c| c as char).collect();
151+
let value_str = header_to_byte_string(value);
150152
let header_name_str = name.as_str();
151153
header_pairs.push((header_name_str.to_string(), value_str));
152154
}
@@ -168,3 +170,18 @@ impl HttpResponseV8 {
168170
))
169171
}
170172
}
173+
174+
// WebIDL ByteStrings use "isomorphic encoding" to convert to/from JS strings,
175+
// i.e. latin-1
176+
fn header_to_byte_string(header: &HeaderValue) -> String {
177+
header.as_bytes().iter().map(|&b| char::from(b)).collect()
178+
}
179+
180+
fn byte_string_to_header(header: &str) -> anyhow::Result<HeaderValue> {
181+
// TODO: turn these into TypeErrors
182+
let bytes = header
183+
.chars()
184+
.map(|c| u8::try_from(c).map_err(|_| anyhow::anyhow!("invalid char for header: `{c}`")))
185+
.collect::<Result<Vec<_>, _>>()?;
186+
Ok(HeaderValue::from_bytes(&bytes)?)
187+
}

npm-packages/udf-tests/convex/fetch.ts

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1781,7 +1781,8 @@ export const fetchUnendingRequest = action(async () => {
17811781
async function fetchOlaf() {
17821782
const response = await fetch("http://localhost:4545/echo_server", {
17831783
method: "POST",
1784-
headers: { "X-Olaf": "⛄" },
1784+
// hacky way to create a UTF-8 encoded byte string
1785+
headers: { "X-Olaf": unescape(encodeURIComponent("⛄")) },
17851786
});
17861787
assert.strictEqual(response.headers.get("X-Olaf"), "â\x9B\x84");
17871788
}

0 commit comments

Comments
 (0)