mirror of
https://github.com/tauri-apps/plugins-workspace.git
synced 2026-10-18 23:07:27 +02:00
fix(http): decode header values as ISO-8859-1 (#3586)
* fix(http): decode response header values as ISO-8859-1
Response header values were decoded with String::from_utf8 before being
sent over IPC. Raw UTF-8 bytes produced JS strings with code points
above 255, which new Headers() rejects as invalid ByteStrings
("Type error" on WebKit), and bytes that were not valid UTF-8 failed
the fetch outright. Decode as ISO-8859-1 instead so every byte maps to a
char 0-255, matching what browsers expose for the same response.
Fixes #2898
* fix(http): add tests for ISO-8859-1 response header decoding
Cover the #2898 Content-Disposition case and non-UTF-8 header bytes on
the same conversion fetch_send uses, so a revert to UTF-8 fails the suite.
* request headers, e2e test
---------
Co-authored-by: Lucas Nogueira <lucas@tauri.app>
This commit is contained in:
1 parent
62b3565fb5
commit
0e17675653
6 files changed
+141
-7
No files matched your search
@@ -24,6 +24,12 @@ export const UPDATER_FIXTURE_OLDER_VERSION = '1.0.0'
|
||||
export const DOWNLOAD_FIXTURE_BODY =
|
||||
'hello from the plugins e2e fixture server\n'.repeat(64)
|
||||
|
||||
/**
|
||||
* Filename `GET /raw-header` puts in its `content-disposition`, sent as raw
|
||||
* UTF-8 bytes on the wire as many servers do.
|
||||
*/
|
||||
export const RAW_HEADER_FILENAME = '中文-EN.pdf'
|
||||
|
||||
/** WebSocket endpoint of the fixture server (the `ws://` twin of {@link FIXTURE_SERVER_URL}). */
|
||||
export const WEBSOCKET_FIXTURE_URL = `ws://127.0.0.1:${FIXTURE_SERVER_PORT}/ws`
|
||||
/** Text message that makes the `/ws` endpoint close the connection with {@link WEBSOCKET_CLOSE_CODE}. */
|
||||
@@ -45,7 +51,10 @@ export interface FixtureServer {
|
||||
* {@link UPDATER_TARGET_NO_UPDATE}.
|
||||
* - `GET /download` — {@link DOWNLOAD_FIXTURE_BODY} with a `Content-Length`.
|
||||
* - `* /echo` — a JSON description of the request (`method`, `url`, `headers`
|
||||
* and the utf-8 `body`).
|
||||
* and the utf-8 `body`). Node decodes header values as ISO-8859-1, one char
|
||||
* per byte, so `headers` shows the exact bytes the client sent.
|
||||
* - `GET /raw-header` — an empty response whose `content-disposition` names
|
||||
* {@link RAW_HEADER_FILENAME} in raw UTF-8 bytes.
|
||||
* - `ws /ws` — a WebSocket echo endpoint: text and binary messages are sent
|
||||
* back as-is, and {@link WEBSOCKET_CLOSE_REQUEST} makes the server close the
|
||||
* connection with {@link WEBSOCKET_CLOSE_CODE}. On `/ws/headers` the server
|
||||
@@ -91,6 +100,20 @@ export function startFixtureServer(): Promise<FixtureServer> {
|
||||
return
|
||||
}
|
||||
|
||||
if (route === 'raw-header' && req.method === 'GET') {
|
||||
// Node writes header strings as ISO-8859-1, so this sends the UTF-8 bytes as-is.
|
||||
const disposition = Buffer.from(
|
||||
`attachment; filename="${RAW_HEADER_FILENAME}"`
|
||||
).toString('latin1')
|
||||
res
|
||||
.writeHead(200, {
|
||||
'content-disposition': disposition,
|
||||
'content-length': 0
|
||||
})
|
||||
.end()
|
||||
return
|
||||
}
|
||||
|
||||
if (route === 'echo') {
|
||||
json(res, {
|
||||
method: req.method,
|
||||
|
||||
@@ -4,6 +4,7 @@
|
||||
|
||||
import { expect } from '@wdio/globals'
|
||||
import { tauri, tauriError, describePlugin } from '../helpers/index.js'
|
||||
import { FIXTURE_SERVER_URL, RAW_HEADER_FILENAME } from '../helpers/server.js'
|
||||
|
||||
// The example spawns an echo server on this port: it replies with the request
|
||||
// body and the request headers, and sets a `session-token` cookie on requests
|
||||
@@ -93,6 +94,40 @@ describePlugin('http', () => {
|
||||
expect(body.text).toContain('qux')
|
||||
})
|
||||
|
||||
it('fetch exposes non-ASCII response header bytes as a ByteString', async () => {
|
||||
// https://github.com/tauri-apps/plugins-workspace/issues/2898
|
||||
const result = await tauri(async (api, url) => {
|
||||
const response = await api.http.fetch(`${url}/raw-header`)
|
||||
await response.text()
|
||||
const value = response.headers.get('content-disposition') ?? ''
|
||||
// A ByteString holds one byte per char, like a browser exposes it.
|
||||
const bytes = Uint8Array.from(value, (c) => c.charCodeAt(0))
|
||||
return new TextDecoder().decode(bytes)
|
||||
}, FIXTURE_SERVER_URL)
|
||||
expect(result).toBe(`attachment; filename="${RAW_HEADER_FILENAME}"`)
|
||||
})
|
||||
|
||||
it('fetch sends ByteString request header values as raw bytes', async () => {
|
||||
// Each char is one byte (0x80-0xFF) on the wire, as in a browser. The
|
||||
// tail is the UTF-8 encoding of `中`, as when a response header is sent back.
|
||||
const value = 'x\u00e4\u00ff\u00e4\u00b8\u00ad'
|
||||
const echoed = await tauri(
|
||||
async (api, url, value) => {
|
||||
const response = await api.http.fetch(`${url}/echo`, {
|
||||
headers: { 'x-e2e-raw': value }
|
||||
})
|
||||
const body = (await response.json()) as {
|
||||
headers: Record<string, string>
|
||||
}
|
||||
return body.headers['x-e2e-raw']
|
||||
},
|
||||
FIXTURE_SERVER_URL,
|
||||
value
|
||||
)
|
||||
// the fixture server decodes header values as ISO-8859-1
|
||||
expect(echoed).toBe(value)
|
||||
})
|
||||
|
||||
it('the cookie jar stores and replays cookies across requests', async () => {
|
||||
const result = await tauri(async (api, url) => {
|
||||
// The jar is persisted in the app data dir, so an earlier run (or the
|
||||
|
||||
Reference in new issue
Block a user