提交 87fd1cd3 编写于 作者: M Mislav Marohnić 提交者: GitHub

Merge pull request #428 from mislav/spec-compat

Spec compatibility for Request/Response constructors and cloning
...@@ -260,7 +260,10 @@ ...@@ -260,7 +260,10 @@
function Request(input, options) { function Request(input, options) {
options = options || {} options = options || {}
var body = options.body var body = options.body
if (Request.prototype.isPrototypeOf(input)) {
if (typeof input === 'string') {
this.url = input
} else {
if (input.bodyUsed) { if (input.bodyUsed) {
throw new TypeError('Already read') throw new TypeError('Already read')
} }
...@@ -271,12 +274,10 @@ ...@@ -271,12 +274,10 @@
} }
this.method = input.method this.method = input.method
this.mode = input.mode this.mode = input.mode
if (!body) { if (!body && input._bodyInit != null) {
body = input._bodyInit body = input._bodyInit
input.bodyUsed = true input.bodyUsed = true
} }
} else {
this.url = input
} }
this.credentials = options.credentials || this.credentials || 'omit' this.credentials = options.credentials || this.credentials || 'omit'
...@@ -294,7 +295,7 @@ ...@@ -294,7 +295,7 @@
} }
Request.prototype.clone = function() { Request.prototype.clone = function() {
return new Request(this) return new Request(this, { body: this._bodyInit })
} }
function decode(body) { function decode(body) {
...@@ -310,16 +311,17 @@ ...@@ -310,16 +311,17 @@
return form return form
} }
function headers(xhr) { function parseHeaders(rawHeaders) {
var head = new Headers() var headers = new Headers()
var pairs = (xhr.getAllResponseHeaders() || '').trim().split('\n') rawHeaders.split('\r\n').forEach(function(line) {
pairs.forEach(function(header) { var parts = line.split(':')
var split = header.trim().split(':') var key = parts.shift().trim()
var key = split.shift().trim() if (key) {
var value = split.join(':').trim() var value = parts.join(':').trim()
head.append(key, value) headers.append(key, value)
}
}) })
return head return headers
} }
Body.call(Request.prototype) Body.call(Request.prototype)
...@@ -333,7 +335,7 @@ ...@@ -333,7 +335,7 @@
this.status = options.status this.status = options.status
this.ok = this.status >= 200 && this.status < 300 this.ok = this.status >= 200 && this.status < 300
this.statusText = options.statusText this.statusText = options.statusText
this.headers = options.headers instanceof Headers ? options.headers : new Headers(options.headers) this.headers = new Headers(options.headers)
this.url = options.url || '' this.url = options.url || ''
this._initBody(bodyInit) this._initBody(bodyInit)
} }
...@@ -371,35 +373,16 @@ ...@@ -371,35 +373,16 @@
self.fetch = function(input, init) { self.fetch = function(input, init) {
return new Promise(function(resolve, reject) { return new Promise(function(resolve, reject) {
var request var request = new Request(input, init)
if (Request.prototype.isPrototypeOf(input) && !init) {
request = input
} else {
request = new Request(input, init)
}
var xhr = new XMLHttpRequest() var xhr = new XMLHttpRequest()
function responseURL() {
if ('responseURL' in xhr) {
return xhr.responseURL
}
// Avoid security warnings on getResponseHeader when not allowed by CORS
if (/^X-Request-URL:/mi.test(xhr.getAllResponseHeaders())) {
return xhr.getResponseHeader('X-Request-URL')
}
return
}
xhr.onload = function() { xhr.onload = function() {
var options = { var options = {
status: xhr.status, status: xhr.status,
statusText: xhr.statusText, statusText: xhr.statusText,
headers: headers(xhr), headers: parseHeaders(xhr.getAllResponseHeaders() || '')
url: responseURL()
} }
options.url = 'responseURL' in xhr ? xhr.responseURL : options.headers.get('X-Request-URL')
var body = 'response' in xhr ? xhr.response : xhr.responseText var body = 'response' in xhr ? xhr.response : xhr.responseText
resolve(new Response(body, options)) resolve(new Response(body, options))
} }
......
...@@ -11,7 +11,7 @@ ...@@ -11,7 +11,7 @@
"jshint": "2.8.0", "jshint": "2.8.0",
"mocha": "2.1.0", "mocha": "2.1.0",
"mocha-phantomjs-core": "2.0.1", "mocha-phantomjs-core": "2.0.1",
"url-search-params": "0.5.0" "url-search-params": "0.6.1"
}, },
"files": [ "files": [
"LICENSE", "LICENSE",
......
...@@ -53,6 +53,16 @@ function readBlobAsBytes(blob) { ...@@ -53,6 +53,16 @@ function readBlobAsBytes(blob) {
} }
} }
function arrayBufferFromText(text) {
var buf = new ArrayBuffer(text.length)
var view = new Uint8Array(buf)
for (var i = 0; i < text.length; i++) {
view[i] = text.charCodeAt(i)
}
return buf
}
var native = {} var native = {}
var keepGlobals = ['fetch', 'Headers', 'Request', 'Response'] var keepGlobals = ['fetch', 'Headers', 'Request', 'Response']
var exercise = ['polyfill'] var exercise = ['polyfill']
...@@ -87,32 +97,6 @@ exercise.forEach(function(exerciseMode) { ...@@ -87,32 +97,6 @@ exercise.forEach(function(exerciseMode) {
var nativeFirefox = /Firefox\//.test(navigator.userAgent) && exerciseMode === 'native' var nativeFirefox = /Firefox\//.test(navigator.userAgent) && exerciseMode === 'native'
var polyfillFirefox = /Firefox\//.test(navigator.userAgent) && exerciseMode === 'polyfill' var polyfillFirefox = /Firefox\//.test(navigator.userAgent) && exerciseMode === 'polyfill'
test('resolves promise on 500 error', function() {
return fetch('/boom').then(function(response) {
assert.equal(response.status, 500)
assert.equal(response.ok, false)
return response.text()
}).then(function(body) {
assert.equal(body, 'boom')
})
})
test.skip('rejects promise for network error', function() {
return fetch('/error').then(function(response) {
assert(false, 'HTTP status ' + response.status + ' was treated as success')
}).catch(function(error) {
assert(error instanceof TypeError, 'Rejected with Error')
})
})
test('rejects when Request constructor throws', function() {
return fetch('/request', { method: 'GET', body: 'invalid' }).then(function() {
assert(false, 'Invalid Request init was accepted')
}).catch(function(error) {
assert(error instanceof TypeError, 'Rejected with Error')
})
})
// https://fetch.spec.whatwg.org/#headers-class // https://fetch.spec.whatwg.org/#headers-class
suite('Headers', function() { suite('Headers', function() {
test('constructor copies headers', function() { test('constructor copies headers', function() {
...@@ -261,69 +245,6 @@ suite('Headers', function() { ...@@ -261,69 +245,6 @@ suite('Headers', function() {
// https://fetch.spec.whatwg.org/#request-class // https://fetch.spec.whatwg.org/#request-class
suite('Request', function() { suite('Request', function() {
test('sends request headers', function() {
return fetch('/request', {
headers: {
'Accept': 'application/json',
'X-Test': '42'
}
}).then(function(response) {
return response.json()
}).then(function(json) {
assert.equal(json.headers['accept'], 'application/json')
assert.equal(json.headers['x-test'], '42')
})
})
test('fetch request', function() {
var request = new Request('/request', {
headers: {
'Accept': 'application/json',
'X-Test': '42'
}
})
return fetch(request).then(function(response) {
return response.json()
}).then(function(json) {
assert.equal(json.headers['accept'], 'application/json')
assert.equal(json.headers['x-test'], '42')
})
})
featureDependent(test, support.arrayBuffer, 'sends ArrayBuffer body', function() {
var text = 'name=Hubot'
var buf = new ArrayBuffer(text.length)
var view = new Uint8Array(buf)
for(var i = 0; i < text.length; i++) {
view[i] = text.charCodeAt(i)
}
return fetch('/request', {
method: 'post',
body: buf
}).then(function(response) {
return response.json()
}).then(function(request) {
assert.equal(request.method, 'POST')
assert.equal(request.data, 'name=Hubot')
})
})
featureDependent(test, support.searchParams, 'sends URLSearchParams body', function() {
return fetch('/request', {
method: 'post',
body: new URLSearchParams('a=1&b=2')
}).then(function(response) {
return response.json()
}).then(function(request) {
assert.equal(request.method, 'POST')
assert.equal(request.data, 'a=1&b=2')
})
})
test('construct with url', function() { test('construct with url', function() {
var request = new Request('https://fetch.spec.whatwg.org/') var request = new Request('https://fetch.spec.whatwg.org/')
assert.equal(request.url, 'https://fetch.spec.whatwg.org/') assert.equal(request.url, 'https://fetch.spec.whatwg.org/')
...@@ -485,9 +406,10 @@ suite('Request', function() { ...@@ -485,9 +406,10 @@ suite('Request', function() {
assert.equal(clone.method, 'POST') assert.equal(clone.method, 'POST')
assert.equal(clone.headers.get('content-type'), 'text/plain') assert.equal(clone.headers.get('content-type'), 'text/plain')
assert.notEqual(clone.headers, req.headers) assert.notEqual(clone.headers, req.headers)
assert.equal(req.bodyUsed, false)
return clone.text().then(function(body) { return Promise.all([clone.text(), req.clone().text()]).then(function(bodies) {
assert.equal(body, 'I work out') assert.deepEqual(bodies, ['I work out', 'I work out'])
}) })
}) })
...@@ -508,14 +430,14 @@ suite('Request', function() { ...@@ -508,14 +430,14 @@ suite('Request', function() {
suite('BodyInit extract', function() { suite('BodyInit extract', function() {
featureDependent(suite, support.blob, 'type Blob', function() { featureDependent(suite, support.blob, 'type Blob', function() {
test('consume as blob', function() { test('consume as blob', function() {
var request = new Request(null, {method: 'POST', body: new Blob(['hello'])}) var request = new Request('', {method: 'POST', body: new Blob(['hello'])})
return request.blob().then(readBlobAsText).then(function(text) { return request.blob().then(readBlobAsText).then(function(text) {
assert.equal(text, 'hello') assert.equal(text, 'hello')
}) })
}) })
test('consume as text', function() { test('consume as text', function() {
var request = new Request(null, {method: 'POST', body: new Blob(['hello'])}) var request = new Request('', {method: 'POST', body: new Blob(['hello'])})
return request.text().then(function(text) { return request.text().then(function(text) {
assert.equal(text, 'hello') assert.equal(text, 'hello')
}) })
...@@ -524,14 +446,14 @@ suite('Request', function() { ...@@ -524,14 +446,14 @@ suite('Request', function() {
suite('type USVString', function() { suite('type USVString', function() {
test('consume as text', function() { test('consume as text', function() {
var request = new Request(null, {method: 'POST', body: 'hello'}) var request = new Request('', {method: 'POST', body: 'hello'})
return request.text().then(function(text) { return request.text().then(function(text) {
assert.equal(text, 'hello') assert.equal(text, 'hello')
}) })
}) })
featureDependent(test, support.blob, 'consume as blob', function() { featureDependent(test, support.blob, 'consume as blob', function() {
var request = new Request(null, {method: 'POST', body: 'hello'}) var request = new Request('', {method: 'POST', body: 'hello'})
return request.blob().then(readBlobAsText).then(function(text) { return request.blob().then(readBlobAsText).then(function(text) {
assert.equal(text, 'hello') assert.equal(text, 'hello')
}) })
...@@ -577,23 +499,6 @@ suite('Response', function() { ...@@ -577,23 +499,6 @@ suite('Response', function() {
}) })
}) })
test('populates response body', function() {
return fetch('/hello').then(function(response) {
assert.equal(response.status, 200)
assert.equal(response.ok, true)
return response.text()
}).then(function(body) {
assert.equal(body, 'hi')
})
})
test('parses response headers', function() {
return fetch('/headers?' + new Date().getTime()).then(function(response) {
assert.equal(response.headers.get('Date'), 'Mon, 13 Oct 2014 21:02:27 GMT')
assert.equal(response.headers.get('Content-Type'), 'text/html; charset=utf-8')
})
})
test('creates Headers object from raw headers', function() { test('creates Headers object from raw headers', function() {
var r = new Response('{"foo":"bar"}', {headers: {'content-type': 'application/json'}}) var r = new Response('{"foo":"bar"}', {headers: {'content-type': 'application/json'}})
assert.equal(r.headers instanceof Headers, true) assert.equal(r.headers instanceof Headers, true)
...@@ -603,6 +508,14 @@ suite('Response', function() { ...@@ -603,6 +508,14 @@ suite('Response', function() {
}) })
}) })
test('always creates a new Headers instance', function() {
var headers = new Headers({ 'x-hello': 'world' })
var res = new Response('', {headers: headers})
assert.equal(res.headers.get('x-hello'), 'world')
assert.notEqual(res.headers, headers)
})
test('clone text response', function() { test('clone text response', function() {
var res = new Response('{"foo":"bar"}', { var res = new Response('{"foo":"bar"}', {
headers: {'content-type': 'application/json'} headers: {'content-type': 'application/json'}
...@@ -618,18 +531,9 @@ suite('Response', function() { ...@@ -618,18 +531,9 @@ suite('Response', function() {
}) })
featureDependent(test, support.blob, 'clone blob response', function() { featureDependent(test, support.blob, 'clone blob response', function() {
return fetch('/binary').then(function(response) { var req = new Request(new Blob(['test']))
return Promise.all([response.clone().arrayBuffer(), response.arrayBuffer()]).then(function(bufs){ req.clone()
bufs.forEach(function(buf){ assert.equal(req.bodyUsed, false)
assert(buf instanceof ArrayBuffer, 'buf is an ArrayBuffer instance')
assert.equal(buf.byteLength, 256, 'buf.byteLength is correct')
var view = new Uint8Array(buf)
for (var i = 0; i < 256; i++) {
assert.equal(view[i], i)
}
})
})
})
}) })
test('error creates error Response', function() { test('error creates error Response', function() {
...@@ -877,8 +781,152 @@ suite('Body mixin', function() { ...@@ -877,8 +781,152 @@ suite('Body mixin', function() {
}) })
}) })
suite('fetch method', function() {
suite('promise resolution', function() {
test('resolves promise on 500 error', function() {
return fetch('/boom').then(function(response) {
assert.equal(response.status, 500)
assert.equal(response.ok, false)
return response.text()
}).then(function(body) {
assert.equal(body, 'boom')
})
})
test.skip('rejects promise for network error', function() {
return fetch('/error').then(function(response) {
assert(false, 'HTTP status ' + response.status + ' was treated as success')
}).catch(function(error) {
assert(error instanceof TypeError, 'Rejected with Error')
})
})
test('rejects when Request constructor throws', function() {
return fetch('/request', { method: 'GET', body: 'invalid' }).then(function() {
assert(false, 'Invalid Request init was accepted')
}).catch(function(error) {
assert(error instanceof TypeError, 'Rejected with Error')
})
})
})
suite('request', function() {
test('sends headers', function() {
return fetch('/request', {
headers: {
'Accept': 'application/json',
'X-Test': '42'
}
}).then(function(response) {
return response.json()
}).then(function(json) {
assert.equal(json.headers['accept'], 'application/json')
assert.equal(json.headers['x-test'], '42')
})
})
test('with Request as argument', function() {
var request = new Request('/request', {
headers: {
'Accept': 'application/json',
'X-Test': '42'
}
})
return fetch(request).then(function(response) {
return response.json()
}).then(function(json) {
assert.equal(json.headers['accept'], 'application/json')
assert.equal(json.headers['x-test'], '42')
})
})
test('reusing same Request multiple times', function() {
var request = new Request('/request', {
headers: {
'Accept': 'application/json',
'X-Test': '42'
}
})
var responses = []
return fetch(request).then(function(response) {
responses.push(response)
return fetch(request)
}).then(function(response) {
responses.push(response)
return fetch(request)
}).then(function(response) {
responses.push(response)
return Promise.all(responses.map(function(r) { return r.json() }))
}).then(function(jsons) {
jsons.forEach(function(json) {
assert.equal(json.headers['accept'], 'application/json')
assert.equal(json.headers['x-test'], '42')
})
})
})
featureDependent(test, support.arrayBuffer, 'sends ArrayBuffer body', function() {
return fetch('/request', {
method: 'post',
body: arrayBufferFromText('name=Hubot')
}).then(function(response) {
return response.json()
}).then(function(request) {
assert.equal(request.method, 'POST')
assert.equal(request.data, 'name=Hubot')
})
})
featureDependent(test, support.searchParams, 'sends URLSearchParams body', function() {
return fetch('/request', {
method: 'post',
body: new URLSearchParams('a=1&b=2')
}).then(function(response) {
return response.json()
}).then(function(request) {
assert.equal(request.method, 'POST')
assert.equal(request.data, 'a=1&b=2')
})
})
})
suite('response', function() {
test('populates body', function() {
return fetch('/hello').then(function(response) {
assert.equal(response.status, 200)
assert.equal(response.ok, true)
return response.text()
}).then(function(body) {
assert.equal(body, 'hi')
})
})
test('parses headers', function() {
return fetch('/headers?' + new Date().getTime()).then(function(response) {
assert.equal(response.headers.get('Date'), 'Mon, 13 Oct 2014 21:02:27 GMT')
assert.equal(response.headers.get('Content-Type'), 'text/html; charset=utf-8')
})
})
featureDependent(test, support.blob, 'handles binary', function() {
return fetch('/binary').then(function(response) {
return response.arrayBuffer()
}).then(function(buf) {
assert(buf instanceof ArrayBuffer, 'buf is an ArrayBuffer instance')
assert.equal(buf.byteLength, 256, 'buf.byteLength is correct')
var view = new Uint8Array(buf)
for (var i = 0; i < 256; i++) {
assert.equal(view[i], i)
}
})
})
})
// https://fetch.spec.whatwg.org/#methods // https://fetch.spec.whatwg.org/#methods
suite('Methods', function() { suite('HTTP methods', function() {
test('supports HTTP GET', function() { test('supports HTTP GET', function() {
return fetch('/request', { return fetch('/request', {
method: 'get', method: 'get',
...@@ -1095,6 +1143,7 @@ suite('credentials mode', function() { ...@@ -1095,6 +1143,7 @@ suite('credentials mode', function() {
}) })
}) })
}) })
})
}) })
}) })
......
Markdown is supported
0% .
You are about to add 0 people to the discussion. Proceed with caution.
先完成此消息的编辑!
想要评论请 注册