Skip to content

Commit

Permalink
Refactor XHR's send() implementation
Browse files Browse the repository at this point in the history
This should improve correctness slightly, but it was mainly motivated by trying to fix a bug that in the end was not related to this code. Even though it did not fix the bug I was chasing, it was a good refactoring, so I thought I'd best commit it anyway.
  • Loading branch information
domenic committed May 20, 2017
1 parent 7d950c4 commit b1b7f23
Showing 1 changed file with 112 additions and 66 deletions.
178 changes: 112 additions & 66 deletions lib/jsdom/living/xmlhttprequest.js
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ const URL = require("whatwg-url").URL;
const whatwgEncoding = require("whatwg-encoding");
const tough = require("tough-cookie");
const parseContentType = require("content-type-parser");
const conversions = require("webidl-conversions");

const xhrUtils = require("./xhr-utils");
const DOMException = require("../web-idl/DOMException");
Expand All @@ -15,6 +16,7 @@ const documentBaseURLSerialized = require("./helpers/document-base-url").documen
const idlUtils = require("./generated/utils");
const Document = require("./generated/Document");
const Blob = require("./generated/Blob");
const FormData = require("./generated/FormData");
const domToHtml = require("../browser/domtohtml").domToHtml;

const syncWorkerFile = require.resolve ? require.resolve("./xhr-sync-worker.js") : null;
Expand Down Expand Up @@ -90,7 +92,6 @@ const simpleHeaders = xhrUtils.simpleHeaders;
module.exports = function createXMLHttpRequest(window) {
const Event = window.Event;
const ProgressEvent = window.ProgressEvent;
const FormData = window.FormData;
const XMLHttpRequestEventTarget = window.XMLHttpRequestEventTarget;
const XMLHttpRequestUpload = window.XMLHttpRequestUpload;

Expand Down Expand Up @@ -534,9 +535,13 @@ module.exports = function createXMLHttpRequest(window) {
}

send(body) {
body = coerceBodyArg(body);

// Not per spec, but per tests: https://github.com/whatwg/xhr/issues/65
if (!this._ownerDocument) {
throw new DOMException(DOMException.INVALID_STATE_ERR);
}

const flag = this[xhrSymbols.flag];
const properties = this[xhrSymbols.properties];

Expand All @@ -547,77 +552,41 @@ module.exports = function createXMLHttpRequest(window) {
properties.beforeSend = true;

try {
if (!flag.body &&
body !== undefined &&
body !== null &&
body !== "" &&
!(flag.method === "HEAD" || flag.method === "GET")) {
let contentType = null;
if (flag.method === "GET" || flag.method === "HEAD") {
body = null;
}

if (body !== null) {
let encoding = null;
if (body instanceof FormData) {
flag.formData = true;
const formData = [];
for (const entry of idlUtils.implForWrapper(body)._entries) {
let val;
if (Blob.isImpl(entry.value)) {
const blob = entry.value;
val = {
name: entry.name,
value: blob._buffer,
options: {
filename: blob.name,
contentType: blob.type,
knownLength: blob.size
}
};
} else {
val = entry;
}
formData.push(val);
}
flag.body = formData;
// TODO content type; what is the form boundary?
} else if (Blob.is(body)) {
const blob = idlUtils.implForWrapper(body);
flag.body = blob._buffer;
if (blob.type !== "") {
contentType = blob.type;
}
} else if (body instanceof ArrayBuffer) {
flag.body = new Buffer(new Uint8Array(body));
} else if (ArrayBuffer.isView(body)) {
flag.body = new Buffer(body.buffer, body.byteOffset, body.byteLength);
} else if (body instanceof Document.interface) {
if (body.childNodes.length === 0) {
throw new DOMException(DOMException.INVALID_STATE_ERR);
}
flag.body = domToHtml([body]);
let mimeType = null;

if (Document.isImpl(body)) {
encoding = "UTF-8";

const documentBodyParsingMode = idlUtils.implForWrapper(body)._parsingMode;
contentType = documentBodyParsingMode === "html" ? "text/html" : "application/xml";
contentType += ";charset=UTF-8";
} else if (typeof body !== "string") {
flag.body = String(body);
mimeType = (body._parsingMode === "html" ? "text/html" : "application/xml") + ";charset=UTF-8";
flag.body = domToHtml([body]);
} else {
flag.body = body;
contentType = "text/plain;charset=UTF-8";
encoding = "UTF-8";
if (typeof body === "string") {
encoding = "UTF-8";
}
const { buffer, formData, contentType } = extractBody(body);
mimeType = contentType;
flag.body = buffer || formData;
flag.formData = Boolean(formData);
}

const existingContentType = xhrUtils.getRequestHeader(flag.requestHeaders, "content-type");
if (contentType !== null && existingContentType === null) {
flag.requestHeaders["Content-Type"] = contentType;
if (mimeType !== null && existingContentType === null) {
flag.requestHeaders["Content-Type"] = mimeType;
} else if (existingContentType !== null && encoding !== null) {
const parsed = parseContentType(existingContentType);
if (parsed) {
parsed.parameterList
.filter(v => v.key && v.key.toLowerCase() === "charset" &&
whatwgEncoding.labelToName(v.value) !== "UTF-8")
.forEach(v => {
v.value = "UTF-8";
});
for (const param of parsed.parameterList) {
if (param.key && param.key.toLowerCase() === "charset") {
if (param.value.toLowerCase() !== encoding.toLowerCase()) {
param.value = encoding;
}
}
}
xhrUtils.updateRequestHeader(flag.requestHeaders, "content-type", parsed.toString());
}
}
Expand All @@ -630,6 +599,11 @@ module.exports = function createXMLHttpRequest(window) {
}
}

// request doesn't like zero-length bodies
if (flag.body && flag.body.byteLength === 0) {
flag.body = null;
}

if (flag.synchronous) {
const flagStr = JSON.stringify(flag, function (k, v) {
if (this === flag && k === "requestManager") {
Expand Down Expand Up @@ -730,10 +704,7 @@ module.exports = function createXMLHttpRequest(window) {
}
}
});
if (body !== undefined &&
body !== null &&
body !== "" &&
!(flag.method === "HEAD" || flag.method === "GET")) {
if (body !== null && body !== "") {
properties.uploadComplete = false;
setDispatchProgressEvents(this);
} else {
Expand Down Expand Up @@ -1098,3 +1069,78 @@ module.exports = function createXMLHttpRequest(window) {

return XMLHttpRequest;
};

function coerceBodyArg(body) {
// Implements the IDL conversion for `optional (Document or BodyInit)? body = null`

if (body === undefined || body === null) {
return null;
}

if (body instanceof ArrayBuffer || ArrayBuffer.isView(body)) {
return body;
}

const impl = idlUtils.implForWrapper(body);
if (impl) {
// TODO: allow URLSearchParams or ReadableStream
if (Blob.isImpl(impl) || FormData.isImpl(impl) || Document.isImpl(impl)) {
return impl;
}
}

return conversions.USVString(body);
}

function extractBody(bodyInit) {
// https://fetch.spec.whatwg.org/#concept-bodyinit-extract
// except we represent the body as a Node.js Buffer instead,
// or a special case for FormData since we want request to handle that. Probably it would be
// cleaner (and allow a future without request) if we did the form encoding ourself.

if (Blob.isImpl(bodyInit)) {
return {
buffer: bodyInit._buffer,
contentType: bodyInit.type === "" ? null : bodyInit.type
};
} else if (bodyInit instanceof ArrayBuffer) {
return {
buffer: new Buffer(new Uint8Array(bodyInit)),
contentType: null
};
} else if (ArrayBuffer.isView(bodyInit)) {
return {
buffer: new Buffer(bodyInit.buffer, bodyInit.byteOffset, bodyInit.byteLength),
contentType: null
};
} else if (FormData.isImpl(bodyInit)) {
const formData = [];
for (const entry of bodyInit._entries) {
let val;
if (Blob.isImpl(entry.value)) {
const blob = entry.value;
val = {
name: entry.name,
value: blob._buffer,
options: {
filename: blob.name,
contentType: blob.type,
knownLength: blob.size
}
};
} else {
val = entry;
}

formData.push(val);
}

return { formData };
}

// Must be a string
return {
buffer: new Buffer(bodyInit, "utf-8"),
contentType: "text/plain;charset=UTF-8"
};
}

0 comments on commit b1b7f23

Please sign in to comment.