Skip to content

Commit f67eb04

Browse files
authored
fix: decode non-ASCII response header values as ISO-8859-1 (#434)
Fixes #430 `HeaderValue::to_str()` rejects any non-ASCII bytes, which caused Node bindings to crash and Python bindings to silently return empty strings when servers sent headers like `last-modified: Dienstag, 31. März 2026`. Replaced with a per-byte ISO-8859-1 decode (`b as char`), which maps bytes 0x00–0xFF directly to Unicode codepoints U+0000–U+00FF. This matches the `obs-text` allowance in [RFC 9110 §5.5](https://www.rfc-editor.org/rfc/rfc9110#section-5.5).
1 parent 0973e70 commit f67eb04

4 files changed

Lines changed: 29 additions & 10 deletions

File tree

impit-node/src/response.rs

Lines changed: 4 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -91,15 +91,10 @@ impl<'env> ImpitResponse {
9191
.to_string();
9292
let mut headers_vec: Vec<(String, String)> = Vec::new();
9393
for (k, v) in response.headers().iter() {
94-
match v.to_str() {
95-
Ok(val) => headers_vec.push((k.as_str().to_string(), val.to_string())),
96-
Err(e) => {
97-
return Err(napi::Error::new(
98-
napi::Status::GenericFailure,
99-
format!("Failed to parse header value for '{}': {:?}", k.as_str(), e),
100-
));
101-
}
102-
}
94+
headers_vec.push((
95+
k.as_str().to_string(),
96+
v.as_bytes().iter().map(|&b| b as char).collect(),
97+
));
10398
}
10499
let headers = Headers(headers_vec);
105100
let ok = response.status().is_success();

impit-node/test/basics.test.ts

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -502,6 +502,11 @@ describe.each([
502502
t.expect(text).toContain(routes.charsetMetaHttpEquiv.bodyString);
503503
});
504504

505+
test('non-ASCII header values are decoded as ISO-8859-1', async (t) => {
506+
const response = await impit.fetch(new URL(routes.nonAsciiHeader.path, "http://127.0.0.1:3001").href);
507+
t.expect(response.headers.get('x-non-ascii')).toBe(routes.nonAsciiHeader.headerValue);
508+
});
509+
505510
test('.json() method works', async (t) => {
506511
const response = await impit.fetch(getHttpBinUrl('/json'));
507512
const json = await response.json();

impit-node/test/mock.server.ts

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,10 @@ export const routes = {
2020
path: '/charset/meta-http-equiv',
2121
bodyString: WIN1250_STRING,
2222
},
23+
nonAsciiHeader: {
24+
path: '/non-ascii-header',
25+
headerValue: 'Dienstag, 31. März 2026',
26+
},
2327
}
2428

2529
export async function runServer(port: number): Promise<Server> {
@@ -50,6 +54,21 @@ export async function runServer(port: number): Promise<Server> {
5054
res.end(html);
5155
});
5256

57+
app.get(routes.nonAsciiHeader.path, (req, res) => {
58+
const socket = res.socket!;
59+
socket.write('HTTP/1.1 200 OK\r\n');
60+
socket.write('Content-Type: text/plain\r\n');
61+
socket.write(Buffer.concat([
62+
Buffer.from('X-Non-Ascii: Dienstag, 31. M'),
63+
Buffer.from([0xE4]), // ä in ISO-8859-1
64+
Buffer.from('rz 2026\r\n'),
65+
]));
66+
socket.write('Content-Length: 2\r\n');
67+
socket.write('\r\n');
68+
socket.write('ok');
69+
socket.end();
70+
});
71+
5372
app.get('/socket', (req, res) => {
5473
const socket = req.socket;
5574
const clientAddress = socket.remoteAddress;

impit-python/src/response.rs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -541,7 +541,7 @@ impl ImpitPyResponse {
541541
let headers = HashMap::from_iter(val.headers().iter().map(|(k, v)| {
542542
(
543543
k.as_str().to_string(),
544-
v.to_str().unwrap_or_default().to_string(),
544+
v.as_bytes().iter().map(|&b| b as char).collect::<String>(),
545545
)
546546
}));
547547

0 commit comments

Comments
 (0)