Bug 687087 part 3. Implement XHR.responseType="moz-chunked-text" and XHR.responseType="moz-chunked-arraybuffer". r=smaug on code changes, rs=smaug on tests.

This commit is contained in:
Jonas Sicking 2011-09-23 18:57:36 -07:00
Родитель d51a0e15c7
Коммит 8fae097f9c
6 изменённых файлов: 426 добавлений и 59 удалений

Просмотреть файл

@ -428,6 +428,7 @@ nsXMLHttpRequest::nsXMLHttpRequest()
mProgressEventWasDelayed(PR_FALSE),
mLoadLengthComputable(PR_FALSE), mLoadTotal(0),
mFirstStartRequestSeen(PR_FALSE),
mInLoadProgressEvent(PR_FALSE),
mResultJSON(JSVAL_VOID),
mResultArrayBuffer(nsnull)
{
@ -723,7 +724,8 @@ nsXMLHttpRequest::DetectCharset()
if (mResponseType != XML_HTTP_RESPONSE_TYPE_DEFAULT &&
mResponseType != XML_HTTP_RESPONSE_TYPE_TEXT &&
mResponseType != XML_HTTP_RESPONSE_TYPE_JSON) {
mResponseType != XML_HTTP_RESPONSE_TYPE_JSON &&
mResponseType != XML_HTTP_RESPONSE_TYPE_CHUNKED_TEXT) {
return NS_OK;
}
@ -819,10 +821,17 @@ NS_IMETHODIMP nsXMLHttpRequest::GetResponseText(nsAString& aResponseText)
aResponseText.Truncate();
if (mResponseType != XML_HTTP_RESPONSE_TYPE_DEFAULT &&
mResponseType != XML_HTTP_RESPONSE_TYPE_TEXT) {
mResponseType != XML_HTTP_RESPONSE_TYPE_TEXT &&
mResponseType != XML_HTTP_RESPONSE_TYPE_CHUNKED_TEXT) {
return NS_ERROR_DOM_INVALID_STATE_ERR;
}
if (mResponseType == XML_HTTP_RESPONSE_TYPE_CHUNKED_TEXT &&
!mInLoadProgressEvent) {
aResponseText.SetIsVoid(PR_TRUE);
return NS_OK;
}
if (!(mState & (XML_HTTP_REQUEST_DONE | XML_HTTP_REQUEST_LOADING))) {
return NS_OK;
}
@ -933,6 +942,12 @@ NS_IMETHODIMP nsXMLHttpRequest::GetResponseType(nsAString& aResponseType)
case XML_HTTP_RESPONSE_TYPE_JSON:
aResponseType.AssignLiteral("moz-json");
break;
case XML_HTTP_RESPONSE_TYPE_CHUNKED_TEXT:
aResponseType.AssignLiteral("moz-chunked-text");
break;
case XML_HTTP_RESPONSE_TYPE_CHUNKED_ARRAYBUFFER:
aResponseType.AssignLiteral("moz-chunked-arraybuffer");
break;
default:
NS_ERROR("Should not happen");
}
@ -962,6 +977,16 @@ NS_IMETHODIMP nsXMLHttpRequest::SetResponseType(const nsAString& aResponseType)
mResponseType = XML_HTTP_RESPONSE_TYPE_TEXT;
} else if (aResponseType.EqualsLiteral("moz-json")) {
mResponseType = XML_HTTP_RESPONSE_TYPE_JSON;
} else if (aResponseType.EqualsLiteral("moz-chunked-text")) {
if (!(mState & XML_HTTP_REQUEST_ASYNC)) {
return NS_ERROR_DOM_INVALID_STATE_ERR;
}
mResponseType = XML_HTTP_RESPONSE_TYPE_CHUNKED_TEXT;
} else if (aResponseType.EqualsLiteral("moz-chunked-arraybuffer")) {
if (!(mState & XML_HTTP_REQUEST_ASYNC)) {
return NS_ERROR_DOM_INVALID_STATE_ERR;
}
mResponseType = XML_HTTP_RESPONSE_TYPE_CHUNKED_ARRAYBUFFER;
}
// If the given value is not the empty string, "arraybuffer",
// "blob", "document", or "text" terminate these steps.
@ -989,20 +1014,29 @@ NS_IMETHODIMP nsXMLHttpRequest::GetResponse(JSContext *aCx, jsval *aResult)
switch (mResponseType) {
case XML_HTTP_RESPONSE_TYPE_DEFAULT:
case XML_HTTP_RESPONSE_TYPE_TEXT:
case XML_HTTP_RESPONSE_TYPE_CHUNKED_TEXT:
{
nsString str;
rv = GetResponseText(str);
if (NS_FAILED(rv)) return rv;
nsStringBuffer* buf;
*aResult = XPCStringConvert::ReadableToJSVal(aCx, str, &buf);
if (buf) {
str.ForgetSharedBuffer();
if (str.IsVoid()) {
*aResult = JSVAL_NULL;
} else {
nsStringBuffer* buf;
*aResult = XPCStringConvert::ReadableToJSVal(aCx, str, &buf);
if (buf) {
str.ForgetSharedBuffer();
}
}
}
break;
case XML_HTTP_RESPONSE_TYPE_ARRAYBUFFER:
if (mState & XML_HTTP_REQUEST_DONE) {
case XML_HTTP_RESPONSE_TYPE_CHUNKED_ARRAYBUFFER:
if ((mResponseType == XML_HTTP_RESPONSE_TYPE_ARRAYBUFFER &&
mState & XML_HTTP_REQUEST_DONE) ||
(mResponseType == XML_HTTP_RESPONSE_TYPE_CHUNKED_ARRAYBUFFER &&
mInLoadProgressEvent)) {
if (!mResultArrayBuffer) {
rv = CreateResponseArrayBuffer(aCx);
NS_ENSURE_SUCCESS(rv, rv);
@ -1604,17 +1638,18 @@ nsXMLHttpRequest::StreamReaderFunc(nsIInputStream* in,
if ((xmlHttpRequest->mResponseType == XML_HTTP_RESPONSE_TYPE_DEFAULT &&
xmlHttpRequest->mResponseXML) ||
xmlHttpRequest->mResponseType == XML_HTTP_RESPONSE_TYPE_ARRAYBUFFER ||
xmlHttpRequest->mResponseType == XML_HTTP_RESPONSE_TYPE_BLOB) {
xmlHttpRequest->mResponseType == XML_HTTP_RESPONSE_TYPE_BLOB ||
xmlHttpRequest->mResponseType == XML_HTTP_RESPONSE_TYPE_CHUNKED_ARRAYBUFFER) {
// Copy for our own use
PRUint32 previousLength = xmlHttpRequest->mResponseBody.Length();
xmlHttpRequest->mResponseBody.Append(fromRawSegment,count);
if (count > 0 && xmlHttpRequest->mResponseBody.Length() == previousLength) {
return NS_ERROR_OUT_OF_MEMORY;
}
}
else if (xmlHttpRequest->mResponseType == XML_HTTP_RESPONSE_TYPE_DEFAULT ||
xmlHttpRequest->mResponseType == XML_HTTP_RESPONSE_TYPE_TEXT ||
xmlHttpRequest->mResponseType == XML_HTTP_RESPONSE_TYPE_JSON) {
} else if (xmlHttpRequest->mResponseType == XML_HTTP_RESPONSE_TYPE_DEFAULT ||
xmlHttpRequest->mResponseType == XML_HTTP_RESPONSE_TYPE_TEXT ||
xmlHttpRequest->mResponseType == XML_HTTP_RESPONSE_TYPE_JSON ||
xmlHttpRequest->mResponseType == XML_HTTP_RESPONSE_TYPE_CHUNKED_TEXT) {
NS_ASSERTION(!xmlHttpRequest->mResponseXML,
"We shouldn't be parsing a doc here");
xmlHttpRequest->AppendToResponseText(fromRawSegment, count);
@ -2991,9 +3026,17 @@ nsXMLHttpRequest::MaybeDispatchProgressEvents(PRBool aFinalProgress)
mLoadTotal = mLoadTransferred;
mLoadLengthComputable = PR_TRUE;
}
mInLoadProgressEvent = PR_TRUE;
DispatchProgressEvent(this, NS_LITERAL_STRING(PROGRESS_STR),
PR_TRUE, mLoadLengthComputable, mLoadTransferred,
mLoadTotal, mLoadTransferred, mLoadTotal);
mInLoadProgressEvent = PR_FALSE;
if (mResponseType == XML_HTTP_RESPONSE_TYPE_CHUNKED_TEXT ||
mResponseType == XML_HTTP_RESPONSE_TYPE_CHUNKED_ARRAYBUFFER) {
mResponseBody.Truncate();
mResponseText.Truncate();
mResultArrayBuffer = nsnull;
}
}
mProgressSinceLastProgressEvent = PR_FALSE;

Просмотреть файл

@ -305,7 +305,9 @@ protected:
XML_HTTP_RESPONSE_TYPE_BLOB,
XML_HTTP_RESPONSE_TYPE_DOCUMENT,
XML_HTTP_RESPONSE_TYPE_TEXT,
XML_HTTP_RESPONSE_TYPE_JSON
XML_HTTP_RESPONSE_TYPE_JSON,
XML_HTTP_RESPONSE_TYPE_CHUNKED_TEXT,
XML_HTTP_RESPONSE_TYPE_CHUNKED_ARRAYBUFFER
} mResponseType;
nsCOMPtr<nsIDOMBlob> mResponseBlob;
@ -350,6 +352,7 @@ protected:
nsCOMPtr<nsITimer> mProgressNotifier;
PRPackedBool mFirstStartRequestSeen;
PRPackedBool mInLoadProgressEvent;
nsCOMPtr<nsIAsyncVerifyRedirectCallback> mRedirectCallback;
nsCOMPtr<nsIChannel> mNewRedirectChannel;

Просмотреть файл

@ -499,6 +499,8 @@ _TEST_FILES2 = \
accesscontrol.resource^headers^ \
invalid_accesscontrol.resource \
invalid_accesscontrol.resource^headers^ \
test_xhr_progressevents.html \
progressserver.sjs \
somedatas.resource \
somedatas.resource^headers^ \
delayedServerEvents.sjs \

Просмотреть файл

@ -0,0 +1,51 @@
const CC = Components.Constructor;
const BinaryInputStream = CC("@mozilla.org/binaryinputstream;1",
"nsIBinaryInputStream",
"setInputStream");
function setReq(req) {
setObjectState("content/base/test/progressserver", req);
}
function getReq() {
var req;
getObjectState("content/base/test/progressserver", function(v) {
req = v;
});
return req;
}
function handleRequest(request, response)
{
var pairs = request.queryString.split('&');
var command = pairs.shift();
var bodyStream = new BinaryInputStream(request.bodyInputStream);
var body = "";
var bodyAvail;
while ((bodyAvail = bodyStream.available()) > 0)
body += String.fromCharCode.apply(null, bodyStream.readByteArray(bodyAvail));
if (command == "open") {
response.processAsync();
setReq(response);
response.setHeader("Cache-Control", "no-cache", false);
pairs.forEach(function (val) {
var [name, value] = val.split('=');
response.setHeader(name, unescape(value), false);
});
response.write(body);
return;
}
if (command == "send") {
getReq().write(body);
}
else if (command == "close") {
getReq().finish();
setReq(null);
}
response.setHeader("Content-Type", "text/plain");
response.write("ok");
}

Просмотреть файл

@ -14,41 +14,6 @@
<script class="testbody" type="text/javascript">
SimpleTest.waitForExplicitFinish();
// test receiving as JSON
function testJSON(aJsonStr, invalid) {
var errorThrown = false;
var anotherErrorThrown = false;
var xhr = new XMLHttpRequest();
var didthrow = false;
try { xhr.responseType = 'moz-json'; } catch (e) { didthrow = true; }
ok(didthrow,
"should have thrown when setting responseType to moz-json before open");
xhr.open("POST", 'responseIdentical.sjs', false);
xhr.responseType = 'moz-json';
xhr.send(aJsonStr);
if (!invalid) {
is(JSON.stringify(xhr.response), aJsonStr);
is(xhr.response, xhr.response, "returning the same object on each access");
}
else {
var didThrow = false;
try { xhr.response } catch(ex) { didThrow = true; }
ok(didThrow, "accessing response should throw");
didThrow = false;
try { xhr.response } catch(ex) { didThrow = true; }
ok(didThrow, "accessing response should throw");
}
}
var jsonStr = '{"title":"aBook","author":"john"}';
testJSON(jsonStr, false);
var invalidJson = '{ "abc": }'
testJSON(invalidJson, true);
var path = "/tests/content/base/test/";
var passFiles = [['file_XHR_pass1.xml', 'GET'],
@ -109,18 +74,25 @@ function checkResponseXMLAccessThrows(xhr) {
try { xhr.responseXML } catch (e) { didthrow = true; }
ok(didthrow, "should have thrown when accessing responseXML");
}
function checkSetResponseTypeThrows(xhr) {
function checkResponseAccessThrows(xhr) {
var didthrow = false;
try { xhr.responseType = 'document'; } catch (e) { didthrow = true; }
ok(didthrow, "should have thrown when accessing responseType");
try { xhr.response } catch (e) { didthrow = true; }
ok(didthrow, "should have thrown when accessing response");
}
function checkSetResponseTypeThrows(xhr, type) {
var didthrow = false;
try { xhr.responseType = type; } catch (e) { didthrow = true; }
ok(didthrow, "should have thrown when setting responseType");
}
xhr = new XMLHttpRequest();
checkSetResponseTypeThrows(xhr);
checkSetResponseTypeThrows(xhr, "document");
xhr.open("GET", 'file_XHR_pass1.xml', false);
checkSetResponseTypeThrows(xhr, "moz-chunked-text");
checkSetResponseTypeThrows(xhr, "moz-chunked-arraybuffer");
xhr.responseType = 'document';
xhr.send(null);
checkSetResponseTypeThrows(xhr);
checkSetResponseTypeThrows(xhr, "document");
is(xhr.status, 200, "wrong status");
checkResponseTextAccessThrows(xhr);
is((new XMLSerializer()).serializeToString(xhr.response.documentElement),
@ -178,16 +150,30 @@ checkResponseXMLAccessThrows(xhr);
ab = xhr.response;
ok(ab != null, "should have a non-null arraybuffer");
arraybuffer_equals_to(ab, "\xaa\xee\0\x03\xff\xff\xff\xff\xbb\xbb\xbb\xbb");
is(xhr.response, xhr.response, "returns the same ArrayBuffer");
// test array buffer GetResult returns the same object
xhr = new XMLHttpRequest();
xhr.open("GET", 'file_XHR_binary1.bin', false);
xhr.responseType = 'arraybuffer';
xhr.send(null)
// test response (responseType='moz-json')
var xhr = new XMLHttpRequest();
xhr.open("POST", 'responseIdentical.sjs', false);
xhr.responseType = 'moz-json';
jsonObjStr = JSON.stringify({title: "aBook", author: "john"});
xhr.send(jsonObjStr);
is(xhr.status, 200, "wrong status");
checkResponseTextAccessThrows(xhr);
checkResponseXMLAccessThrows(xhr);
is(xhr.response, xhr.response, "returns the same ArrayBuffer");
is(JSON.stringify(xhr.response), jsonObjStr, "correct result");
is(xhr.response, xhr.response, "returning the same object on each access");
// with invalid json
var xhr = new XMLHttpRequest();
xhr.open("POST", 'responseIdentical.sjs', false);
xhr.responseType = 'moz-json';
xhr.send("{");
is(xhr.status, 200, "wrong status");
checkResponseTextAccessThrows(xhr);
checkResponseXMLAccessThrows(xhr);
checkResponseAccessThrows(xhr);
checkResponseAccessThrows(xhr); // Check twice to ensure that we still throw
// test response (responseType='blob')
var onloadCount = 0;

Просмотреть файл

@ -0,0 +1,282 @@
<!DOCTYPE HTML>
<html>
<head>
<title>Test for XMLHttpRequest Progress Events</title>
<script type="text/javascript" src="/MochiKit/packed.js"></script>
<script type="text/javascript" src="/tests/SimpleTest/SimpleTest.js"></script>
<link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css" />
</head>
<body onload="gen.next();">
<pre id=l></pre>
<script type="application/javascript;version=1.7">
SimpleTest.waitForExplicitFinish();
var gen = runTests();
function log(s) {
//document.getElementById("l").textContent += s + "\n";
}
function getEvent(e) {
log("got event: " + e.type + " (" + e.target.readyState + ")");
gen.send(e);
}
function startsWith(a, b) {
return a.substr(0, b.length) === b;
}
function updateProgress(e, data, testName) {
var test = " while running " + testName;
is(e.type, "progress", "event type" + test);
let response;
if (data.nodata) {
is(e.target.response, null, "response should be null" + test);
response = null;
}
else if (data.text) {
is(typeof e.target.response, "string", "response should be a string" + test);
response = e.target.response;
}
else {
ok(e.target.response instanceof ArrayBuffer, "response should be a ArrayBuffer" + test);
response = bufferToString(e.target.response);
}
if (!data.nodata && !data.encoded) {
if (!data.chunked) {
is(e.loaded, response.length, "event.loaded matches response size" + test);
}
else {
is(e.loaded - data.receivedBytes, response.length,
"event.loaded grew by response size" + test);
}
}
ok(e.loaded > data.receivedBytes, "event.loaded increased" + test);
ok(e.loaded - data.receivedBytes <= data.pendingBytes,
"event.loaded didn't increase too much" + test);
if (!data.nodata) {
var newData;
ok(startsWith(response, data.receivedResult),
"response strictly grew" + test);
newData = response.substr(data.receivedResult.length);
if (!data.encoded) {
ok(newData.length > 0, "sanity check for progress" + test);
}
ok(startsWith(data.pendingResult, newData), "new data matches expected" + test);
}
is(e.lengthComputable, "total" in data, "lengthComputable" + test);
if ("total" in data) {
is(e.total, data.total, "total" + test);
}
if (!data.nodata) {
data.pendingResult = data.pendingResult.substr(newData.length);
}
data.pendingBytes -= e.loaded - data.receivedBytes;
data.receivedResult = response;
data.receivedBytes = e.loaded;
}
function sendData(s) {
var xhr = new XMLHttpRequest();
xhr.open("POST", "progressserver.sjs?send");
xhr.sendAsBinary(s);
}
function closeConn() {
log("in closeConn");
var xhr = new XMLHttpRequest();
xhr.open("POST", "progressserver.sjs?close");
xhr.send();
}
var longString = "long";
while(longString.length < 65536)
longString += longString;
function utf8encode(s) {
return unescape(encodeURIComponent(s));
}
function bufferToString(buffer) {
return String.fromCharCode.apply(String, new Uint8Array(buffer));
}
function runTests() {
var xhr = new XMLHttpRequest();
xhr.onprogress = xhr.onload = xhr.onerror = xhr.onreadystatechange = xhr.onloadend = getEvent;
var responseTypes = [{ type: "text", text: true },
{ type: "arraybuffer", text: false, nodata: true },
{ type: "blob", text: false, nodata: true },
{ type: "document", text: true, nodata: true },
{ type: "moz-json", text: true, nodata: true },
{ type: "", text: true },
{ type: "moz-chunked-text", text: true, chunked: true },
{ type: "moz-chunked-arraybuffer", text: false, chunked: true },
];
var responseType;
while (responseType = responseTypes.shift()) {
let tests = [{ open: "Content-Type=text/plain", name: "simple test" },
{ data: "hello world" },
{ data: "\u0000\u0001\u0002\u0003" },
{ data: longString },
{ data: "x" },
{ close: true },
{ open: "Content-Type=text/plain&Content-Length=20", name: "with length", total: 20 },
// 5 bytes from the "ready" in the open step
{ data: "abcde" },
{ data: "0123456789" },
{ close: true },
{ open: "Content-Type=application/xml", data: "ready", name: "without length, as xml" },
{ data: "<out>" },
{ data: "text" },
{ data: "</foo>invalid" },
{ close: true },
{ open: "Content-Type=text/plain;charset%3dutf-8", name: "utf8 data", encoded: true },
{ data: utf8encode("räksmörgås"), utf16: "räksmörgås" },
{ data: utf8encode("Å").substr(0,1), utf16: "" },
{ data: utf8encode("Å").substr(1), utf16: "Å" },
{ data: utf8encode("aöb").substr(0,2), utf16: "a" },
{ data: utf8encode("aöb").substr(2), utf16: "öb" },
{ data: utf8encode("a\u867Eb").substr(0,3), utf16: "a" },
{ data: utf8encode("a\u867Eb").substr(3,1), utf16: "\u867E" },
{ data: utf8encode("a\u867Eb").substr(4), utf16: "b" },
{ close: true },
];
let testState = { index: 0 };
for (let i = 0; i < tests.length; ++i) {
let test = tests[i];
testState.index++;
if ("open" in test) {
log("opening " + testState.name);
testState = { name: test.name + " for " + responseType.type,
index: 0,
pendingResult: "ready",
pendingBytes: 5,
receivedResult: "",
receivedBytes: 0,
total: test.total,
encoded: test.encoded,
nodata: responseType.nodata,
chunked: responseType.chunked,
text: responseType.text };
xhr.onreadystatechange = null;
xhr.open("POST", "progressserver.sjs?open&" + test.open);
xhr.responseType = responseType.type;
xhr.send("ready");
xhr.onreadystatechange = getEvent;
let e = yield;
is(e.type, "readystatechange", "should readystate to headers-received starting " + testState.name);
is(xhr.readyState, xhr.HEADERS_RECEIVED, "should be in state HEADERS_RECEIVED starting " + testState.name);
e = yield;
is(e.type, "readystatechange", "should readystate to loading starting " + testState.name);
is(xhr.readyState, xhr.LOADING, "should be in state LOADING starting " + testState.name);
if (typeof testState.total == "undefined")
delete testState.total;
}
else if ("close" in test) {
log("closing");
closeConn();
e = yield;
is(e.type, "readystatechange", "should readystate to done closing " + testState.name);
is(xhr.readyState, xhr.DONE, "should be in state DONE closing " + testState.name);
log("readystate to 4");
if (responseType.chunked) {
xhr.responseType;
is(xhr.response, null, "chunked data has null response for " + testState.name);
}
e = yield;
is(e.type, "load", "should fire load closing " + testState.name);
is(e.lengthComputable, true, "length should be computable during load closing " + testState.name);
log("got load");
if (responseType.chunked) {
is(xhr.response, null, "chunked data has null response for " + testState.name);
}
e = yield;
is(e.type, "loadend", "should fire loadend closing " + testState.name);
is(e.lengthComputable, true, "length should be computable during loadend closing " + testState.name);
log("got loadend");
if (responseType.chunked) {
is(xhr.response, null, "chunked data has null response for " + testState.name);
}
if (!testState.nodata || responseType.chunked) {
// This branch intentionally left blank
// Under these conditions we check the response during updateProgress
}
else if (responseType.type === "arraybuffer") {
is(bufferToString(xhr.response), testState.pendingResult,
"full response for " + testState.name);
}
else if (responseType.type === "blob") {
let reader = new FileReader;
reader.readAsBinaryString(xhr.response);
reader.onloadend = getEvent;
yield;
is(reader.result, testState.pendingResult,
"full response in blob for " + testState.name);
}
testState.name = "";
}
else {
log("sending");
if (responseType.text) {
testState.pendingResult += "utf16" in test ? test.utf16 : test.data;
}
else {
testState.pendingResult += test.data;
}
testState.pendingBytes = test.data.length;
sendData(test.data);
}
while(testState.pendingBytes) {
log("waiting for more bytes: " + testState.pendingBytes);
e = yield;
// Readystate can fire several times between each progress event.
if (e.type === "readystatechange")
continue;
updateProgress(e, testState, "data for " + testState.name + "[" + testState.index + "]");
if (responseType.chunked) {
testState.receivedResult = "";
}
}
if (!testState.nodata) {
is(testState.pendingResult, "",
"should have consumed the expected result");
}
log("done with this test");
}
is(testState.name, "", "forgot to close last test");
}
SimpleTest.finish();
yield;
}
</script>
</body>
</html>