Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

fix(fetch): missing body-mixin brand checks #1498

Merged
merged 2 commits into from Jun 18, 2022
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
150 changes: 91 additions & 59 deletions lib/fetch/body.js
Expand Up @@ -262,100 +262,132 @@ function cloneBody (body) {
}
}

const methods = {
async blob () {
const chunks = []
function bodyMixinMethods (instance) {
const methods = {
async blob () {
if (!(this instanceof instance)) {
throw new TypeError('Illegal invocation')
}

if (this[kState].body) {
if (isUint8Array(this[kState].body)) {
chunks.push(this[kState].body)
} else {
const stream = this[kState].body.stream
const chunks = []

if (util.isDisturbed(stream)) {
throw new TypeError('disturbed')
}
if (this[kState].body) {
if (isUint8Array(this[kState].body)) {
chunks.push(this[kState].body)
} else {
const stream = this[kState].body.stream

if (stream.locked) {
throw new TypeError('locked')
}
if (util.isDisturbed(stream)) {
throw new TypeError('disturbed')
}

if (stream.locked) {
throw new TypeError('locked')
}

// Compat.
stream[kBodyUsed] = true
// Compat.
stream[kBodyUsed] = true

for await (const chunk of stream) {
chunks.push(chunk)
for await (const chunk of stream) {
chunks.push(chunk)
}
}
}
}

return new Blob(chunks, { type: this.headers.get('Content-Type') || '' })
},
return new Blob(chunks, { type: this.headers.get('Content-Type') || '' })
},

async arrayBuffer () {
const blob = await this.blob()
return await blob.arrayBuffer()
},
async arrayBuffer () {
if (!(this instanceof instance)) {
throw new TypeError('Illegal invocation')
}

async text () {
const blob = await this.blob()
return toUSVString(await blob.text())
},
const blob = await this.blob()
return await blob.arrayBuffer()
},

async json () {
return JSON.parse(await this.text())
},
async text () {
if (!(this instanceof instance)) {
throw new TypeError('Illegal invocation')
}

const blob = await this.blob()
return toUSVString(await blob.text())
},

async formData () {
const contentType = this.headers.get('Content-Type')

// If mimeType’s essence is "multipart/form-data", then:
if (/multipart\/form-data/.test(contentType)) {
throw new NotSupportedError('multipart/form-data not supported')
} else if (/application\/x-www-form-urlencoded/.test(contentType)) {
// Otherwise, if mimeType’s essence is "application/x-www-form-urlencoded", then:

// 1. Let entries be the result of parsing bytes.
let entries
try {
entries = new URLSearchParams(await this.text())
} catch (err) {
// istanbul ignore next: Unclear when new URLSearchParams can fail on a string.
// 2. If entries is failure, then throw a TypeError.
throw Object.assign(new TypeError(), { cause: err })
async json () {
if (!(this instanceof instance)) {
throw new TypeError('Illegal invocation')
}

// 3. Return a new FormData object whose entries are entries.
const formData = new FormData()
for (const [name, value] of entries) {
formData.append(name, value)
return JSON.parse(await this.text())
},

async formData () {
if (!(this instanceof instance)) {
throw new TypeError('Illegal invocation')
}

const contentType = this.headers.get('Content-Type')

// If mimeType’s essence is "multipart/form-data", then:
if (/multipart\/form-data/.test(contentType)) {
throw new NotSupportedError('multipart/form-data not supported')
} else if (/application\/x-www-form-urlencoded/.test(contentType)) {
// Otherwise, if mimeType’s essence is "application/x-www-form-urlencoded", then:

// 1. Let entries be the result of parsing bytes.
let entries
try {
entries = new URLSearchParams(await this.text())
} catch (err) {
// istanbul ignore next: Unclear when new URLSearchParams can fail on a string.
// 2. If entries is failure, then throw a TypeError.
throw Object.assign(new TypeError(), { cause: err })
}

// 3. Return a new FormData object whose entries are entries.
const formData = new FormData()
for (const [name, value] of entries) {
formData.append(name, value)
}
return formData
} else {
// Otherwise, throw a TypeError.
throw new TypeError()
}
return formData
} else {
// Otherwise, throw a TypeError.
throw new TypeError()
}
}

return methods
}

const properties = {
body: {
enumerable: true,
get () {
if (!this || !this[kState]) {
throw new TypeError('Illegal invocation')
}

return this[kState].body ? this[kState].body.stream : null
}
},
bodyUsed: {
enumerable: true,
get () {
if (!this || !this[kState]) {
throw new TypeError('Illegal invocation')
}

return !!this[kState].body && util.isDisturbed(this[kState].body.stream)
}
}
}

function mixinBody (prototype) {
Object.assign(prototype, methods)
Object.defineProperties(prototype, properties)
Object.assign(prototype.prototype, bodyMixinMethods(prototype))
Object.defineProperties(prototype.prototype, properties)
}

module.exports = {
Expand Down
2 changes: 1 addition & 1 deletion lib/fetch/request.js
Expand Up @@ -748,7 +748,7 @@ class Request {
}
}

mixinBody(Request.prototype)
mixinBody(Request)

function makeRequest (init) {
// https://fetch.spec.whatwg.org/#requests
Expand Down
2 changes: 1 addition & 1 deletion lib/fetch/response.js
Expand Up @@ -287,7 +287,7 @@ class Response {
return clonedResponseObject
}
}
mixinBody(Response.prototype)
mixinBody(Response)

Object.defineProperties(Response.prototype, {
type: kEnumerableProperty,
Expand Down
32 changes: 31 additions & 1 deletion test/fetch/request.js
Expand Up @@ -9,7 +9,7 @@ const {
} = require('../../')
const { kState } = require('../../lib/fetch/symbols.js')

test('arg validation', (t) => {
test('arg validation', async (t) => {
// constructor
t.throws(() => {
// eslint-disable-next-line
Expand Down Expand Up @@ -144,6 +144,16 @@ test('arg validation', (t) => {
Request.prototype.signal.toString()
}, TypeError)

t.throws(() => {
// eslint-disable-next-line no-unused-expressions
Request.prototype.body
}, TypeError)

t.throws(() => {
// eslint-disable-next-line no-unused-expressions
Request.prototype.bodyUsed
}, TypeError)

t.throws(() => {
Request.prototype.clone.call(null)
}, TypeError)
Expand All @@ -152,6 +162,26 @@ test('arg validation', (t) => {
Request.prototype[Symbol.toStringTag].charAt(0)
})

for (const method of [
'text',
'json',
'arrayBuffer',
'blob',
'formData'
]) {
await t.rejects(async () => {
await new Request('http://localhost')[method].call({
blob () {
return {
text () {
return Promise.resolve('emulating this')
}
}
}
})
}, TypeError)
}

t.end()
})

Expand Down
24 changes: 23 additions & 1 deletion test/fetch/response.js
Expand Up @@ -5,7 +5,7 @@ const {
Response
} = require('../../')

test('arg validation', (t) => {
test('arg validation', async (t) => {
// constructor
t.throws(() => {
// eslint-disable-next-line
Expand Down Expand Up @@ -77,10 +77,32 @@ test('arg validation', (t) => {
Response.prototype.headers.toString()
}, TypeError)

t.throws(() => {
// eslint-disable-next-line no-unused-expressions
Response.prototype.body
}, TypeError)

t.throws(() => {
// eslint-disable-next-line no-unused-expressions
Response.prototype.bodyUsed
}, TypeError)

t.throws(() => {
Response.prototype.clone.call(null)
}, TypeError)

await t.rejects(async () => {
await new Response('http://localhost').text.call({
blob () {
return {
text () {
return Promise.resolve('emulating response.blob()')
}
}
}
})
}, TypeError)

t.end()
})

Expand Down