Skip to content

Commit

Permalink
fix: Handle data: URIs more consistently
Browse files Browse the repository at this point in the history
1. No longer requires `;base64` to be included on `data:` URIs.
2. Supports immediately aborting `data:` URIs.

Fix: #18
  • Loading branch information
isaacs committed Nov 8, 2021
1 parent 05fb45b commit 229649c
Show file tree
Hide file tree
Showing 2 changed files with 71 additions and 12 deletions.
40 changes: 29 additions & 11 deletions lib/index.js
Expand Up @@ -17,23 +17,41 @@ const AbortError = require('./abort-error.js')

const resolveUrl = Url.resolve

const fetch = (url, opts) => {
// XXX this should really be split up and unit-ized for easier testing
// and better DRY implementation of data/http request aborting
const fetch = async (url, opts) => {
if (/^data:/.test(url)) {
const request = new Request(url, opts)
try {
const split = url.split(',')
const data = Buffer.from(split[1], 'base64')
const type = split[0].match(/^data:(.*);base64$/)[1]
return Promise.resolve(new Response(data, {
// delay 1 promise tick so that the consumer can abort right away
return Promise.resolve().then(() => new Promise((resolve, reject) => {
let type, data
try {
const { pathname, search, hash } = new Url.URL(url)
const split = pathname.split(',')
if (split.length < 2) {
throw new Error('content-type required for data: URIs')
}
const mime = split.shift()
const base64 = /;base64$/.test(mime)
type = base64 ? mime.slice(0, -1 * ';base64'.length) : mime
const rawData = split.join(',') + search + hash
data = base64 ? Buffer.from(rawData, 'base64') : Buffer.from(rawData)
} catch (er) {
return reject(new FetchError(`[${request.method}] ${
request.url} invalid URL, ${er.message}`, 'system', er))
}

const { signal } = request
if (signal && signal.aborted)
return reject(new AbortError('The user aborted a request.'))

return resolve(new Response(data, {
headers: {
'Content-Type': type,
'Content-Length': data.length,
}
},
}))
} catch (er) {
return Promise.reject(new FetchError(`[${request.method}] ${
request.url} invalid URL, ${er.message}`, 'system', er))
}
}))
}

return new Promise((resolve, reject) => {
Expand Down
43 changes: 42 additions & 1 deletion test/index.js
Expand Up @@ -1868,12 +1868,53 @@ t.test('data uri', t => {

t.test('reject invalid data uri', t =>
t.rejects(fetch(invalidDataUrl), {
message: 'invalid URL',
message: 'content-type required for data: URIs',
}))

t.test('data uri not base64 encoded', t =>
fetch('data:text/plain,hello, world!').then(r => {
t.equal(r.status, 200)
t.equal(r.headers.get('Content-Type'), 'text/plain')
return r.buffer().then(b => t.equal(b.toString(), 'hello, world!'))
}))

t.end()
})

t.test('aborting data uris', t => {
const controllers = [AbortController, AbortController2]
t.plan(controllers.length)
const url = 'data:text/plain;base64,SGVsbG8sIFdvcmxkIQ=='
controllers.forEach((Controller, idx) => {
t.test(`controller ${idx}`, async t => {
t.test('pre-abort', async t => {
const controller = new Controller()
controller.abort()
t.rejects(fetch(url, { signal: controller.signal }), {
message: 'The user aborted a request.',
})
})

t.test('post-abort', async t => {
const controller = new Controller()
t.rejects(fetch(url, { signal: controller.signal }), {
message: 'The user aborted a request.',
})
controller.abort()
})

t.test('cannot abort after first tick', t => {
const controller = new Controller()
t.resolves(fetch(url, { signal: controller.signal }))
Promise.resolve().then(() => {
controller.abort()
t.end()
})
})
})
})
})

t.test('redirect changes host header', t =>
fetch(`http://127.0.0.1:${local.port}/host-redirect`, {
redirect: 'follow',
Expand Down

0 comments on commit 229649c

Please sign in to comment.