Skip to content

Commit c0ba75f

Browse files
authored
fix: content-disposition header parsing (#1911)
1 parent 7827031 commit c0ba75f

File tree

3 files changed

+102
-4
lines changed

3 files changed

+102
-4
lines changed

lib/core/util.js

Lines changed: 20 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -213,25 +213,42 @@ function parseHeaders (headers, obj = {}) {
213213
for (let i = 0; i < headers.length; i += 2) {
214214
const key = headers[i].toString().toLowerCase()
215215
let val = obj[key]
216+
217+
const encoding = key.length === 19 && key === 'content-disposition'
218+
? 'latin1'
219+
: 'utf8'
220+
216221
if (!val) {
217222
if (Array.isArray(headers[i + 1])) {
218223
obj[key] = headers[i + 1]
219224
} else {
220-
obj[key] = headers[i + 1].toString()
225+
obj[key] = headers[i + 1].toString(encoding)
221226
}
222227
} else {
223228
if (!Array.isArray(val)) {
224229
val = [val]
225230
obj[key] = val
226231
}
227-
val.push(headers[i + 1].toString())
232+
val.push(headers[i + 1].toString(encoding))
228233
}
229234
}
230235
return obj
231236
}
232237

233238
function parseRawHeaders (headers) {
234-
return headers.map(header => header.toString())
239+
const ret = []
240+
for (let n = 0; n < headers.length; n += 2) {
241+
const key = headers[n + 0].toString()
242+
243+
const encoding = key.length === 19 && key.toLowerCase() === 'content-disposition'
244+
? 'latin1'
245+
: 'utf8'
246+
247+
const val = headers[n + 1].toString(encoding)
248+
249+
ret.push(key, val)
250+
}
251+
return ret
235252
}
236253

237254
function isBuffer (buffer) {

lib/mock/mock-utils.js

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -188,7 +188,11 @@ function buildKey (opts) {
188188
}
189189

190190
function generateKeyValues (data) {
191-
return Object.entries(data).reduce((keyValuePairs, [key, value]) => [...keyValuePairs, key, value], [])
191+
return Object.entries(data).reduce((keyValuePairs, [key, value]) => [
192+
...keyValuePairs,
193+
Buffer.from(`${key}`),
194+
Array.isArray(value) ? value.map(x => Buffer.from(`${x}`)) : Buffer.from(`${value}`)
195+
], [])
192196
}
193197

194198
/**

test/issue-1903.js

Lines changed: 77 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,77 @@
1+
'use strict'
2+
3+
const { createServer } = require('http')
4+
const { test } = require('tap')
5+
const { request } = require('..')
6+
7+
function createPromise () {
8+
const result = {}
9+
result.promise = new Promise((resolve) => {
10+
result.resolve = resolve
11+
})
12+
return result
13+
}
14+
15+
test('should parse content-disposition consistencely', async (t) => {
16+
t.plan(5)
17+
18+
// create promise to allow server spinup in parallel
19+
const spinup1 = createPromise()
20+
const spinup2 = createPromise()
21+
const spinup3 = createPromise()
22+
23+
// variables to store content-disposition header
24+
const header = []
25+
26+
const server = createServer((req, res) => {
27+
res.writeHead(200, {
28+
'content-length': 2,
29+
'content-disposition': "attachment; filename='år.pdf'"
30+
})
31+
header.push("attachment; filename='år.pdf'")
32+
res.end('OK', spinup1.resolve)
33+
})
34+
t.teardown(server.close.bind(server))
35+
server.listen(0, spinup1.resolve)
36+
37+
const proxy1 = createServer(async (req, res) => {
38+
const { statusCode, headers, body } = await request(`http://localhost:${server.address().port}`, {
39+
method: 'GET'
40+
})
41+
header.push(headers['content-disposition'])
42+
delete headers['transfer-encoding']
43+
res.writeHead(statusCode, headers)
44+
body.pipe(res)
45+
})
46+
t.teardown(proxy1.close.bind(proxy1))
47+
proxy1.listen(0, spinup2.resolve)
48+
49+
const proxy2 = createServer(async (req, res) => {
50+
const { statusCode, headers, body } = await request(`http://localhost:${proxy1.address().port}`, {
51+
method: 'GET'
52+
})
53+
header.push(headers['content-disposition'])
54+
delete headers['transfer-encoding']
55+
res.writeHead(statusCode, headers)
56+
body.pipe(res)
57+
})
58+
t.teardown(proxy2.close.bind(proxy2))
59+
proxy2.listen(0, spinup3.resolve)
60+
61+
// wait until all server spinup
62+
await Promise.all([spinup1.promise, spinup2.promise, spinup3.promise])
63+
64+
const { statusCode, headers, body } = await request(`http://localhost:${proxy2.address().port}`, {
65+
method: 'GET'
66+
})
67+
header.push(headers['content-disposition'])
68+
t.equal(statusCode, 200)
69+
t.equal(await body.text(), 'OK')
70+
71+
// we check header
72+
// must not be the same in first proxy
73+
t.notSame(header[0], header[1])
74+
// chaining always the same value
75+
t.equal(header[1], header[2])
76+
t.equal(header[2], header[3])
77+
})

0 commit comments

Comments
 (0)