Skip to content
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.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
7 changes: 4 additions & 3 deletions packages/devextreme-angular/src/http/ajax.ts
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ import { isDefined } from 'devextreme/core/utils/type';
import { getWindow } from 'devextreme/core/utils/window';
import {
isCrossDomain,
isFormData,
evalCrossDomainScript,
getRequestOptions,
getJsonpCallbackName,
Expand Down Expand Up @@ -101,7 +102,7 @@ function getRequestHeaders(options: Options) {
headers.Accept = getAcceptHeader(options);
}

if (!upload && !isGetMethod(options) && !headers[CONTENT_TYPE]) {
if (!upload && !isFormData(options.data) && !isGetMethod(options) && !headers[CONTENT_TYPE]) {
Comment thread
dmlvr marked this conversation as resolved.
Resolved
headers[CONTENT_TYPE] = options.contentType || `${URLENCODED};charset=utf-8`;
}

Expand Down Expand Up @@ -266,11 +267,11 @@ export const sendRequestFactory = (httpClient: HttpClient) => (options: Options)
return result;
}

if (options.cache === false && isGet && data) {
if (options.cache === false && isGet && data && !isFormData(data)) {
data._ = Date.now() + 1;
}

const makeBody = () => (!upload && typeof data === 'object' && headers[CONTENT_TYPE].indexOf(URLENCODED) === 0
const makeBody = () => (!upload && !isFormData(data) && typeof data === 'object' && headers[CONTENT_TYPE].indexOf(URLENCODED) === 0
Comment thread
dmlvr marked this conversation as resolved.
Resolved
? Object.keys(data).reduce(
(httpParams, key) => httpParams.set(key, data[key]),
new HttpParams(),
Expand Down
39 changes: 39 additions & 0 deletions packages/devextreme-angular/tests/src/http/ajax.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -123,6 +123,45 @@ describe('Ajax request using DxHttpModule', () => {
httpTestingControllerMock.expectOne(url);
});

it('FormData without the upload option is sent as is', () => {
const url = 'http://somefakedomain1221.com/form-data-url';
const formData = new FormData();
formData.append('name', 'test');

ajax.sendRequest({
url,
method: 'POST',
data: formData,
});

const req = httpTestingControllerMock.expectOne(url);

expect(req.request.body).toBe(formData);
expect(req.request.headers.get('Content-Type')).toBeNull();

req.flush({});
});

it('a GET with FormData and cache: false does not modify the caller FormData', () => {
const url = 'http://somefakedomain1221.com/form-data-get';
const formData = new FormData();
formData.append('name', 'test');

ajax.sendRequest({
url,
method: 'GET',
cache: false,
data: formData,
});

const req = httpTestingControllerMock.expectOne((r) => r.url === url);

expect('_' in formData).toBe(false);
expect(req.request.params.keys()).toEqual([]);

req.flush({});
});

it('remote provider should upload a file', (done) => {
const url = 'http://somefakedomain1221.com/json-url';

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -8,9 +8,11 @@ import {
getRequestOptions,
isCrossDomain,
} from '@js/core/utils/ajax_utils';
import type { DeferredObj } from '@js/core/utils/deferred';
import { Deferred } from '@js/core/utils/deferred';
import { isDefined } from '@js/core/utils/type';
import { getWindow } from '@js/core/utils/window';
import type { AjaxRequestOptions } from '@ts/core/utils/ajax_utils';
import { injector } from '@ts/core/utils/dependency_injector';

const window = getWindow();
Expand All @@ -21,36 +23,40 @@ const TIMEOUT = 'timeout';
const NO_CONTENT = 'nocontent';
const PARSER_ERROR = 'parsererror';

const isStatusSuccess = function (status) {
return status >= 200 && status < 300;
};
type AjaxXhr = XMLHttpRequest & { customStatus?: string };

const hasContent = function (status) {
return status !== 204;
};
type AjaxRequestResult = Promise<unknown> & { abort?: () => void };

const getDataFromResponse = function (xhr) {
return xhr.responseType && xhr.responseType !== 'text' || typeof xhr.responseText !== 'string'
? xhr.response
: xhr.responseText;
};
const isStatusSuccess = (status: number): boolean => status >= 200 && status < 300;

const hasContent = (status: number): boolean => status !== 204;

const postProcess = function (deferred, xhr, dataType) {
const getDataFromResponse = (xhr: AjaxXhr): unknown => (
(xhr.responseType && xhr.responseType !== 'text') || typeof xhr.responseText !== 'string'
? xhr.response
: xhr.responseText
);

const postProcess = (
deferred: DeferredObj<unknown>,
xhr: AjaxXhr,
dataType: string | undefined,
): void => {
const data = getDataFromResponse(xhr);

switch (dataType) {
case 'jsonp':
evalScript(data);
evalScript(data as string);
break;

case 'script':
evalScript(data);
evalScript(data as string);
deferred.resolve(data, SUCCESS, xhr);
break;

case 'json':
try {
deferred.resolve(JSON.parse(data), SUCCESS, xhr);
deferred.resolve(JSON.parse(data as string), SUCCESS, xhr);
} catch (e) {
deferred.reject(xhr, PARSER_ERROR, e);
}
Expand All @@ -61,26 +67,33 @@ const postProcess = function (deferred, xhr, dataType) {
}
};

const setHttpTimeout = function (timeout, xhr) {
return timeout && setTimeout(function () {
type TimeoutId = ReturnType<typeof setTimeout>;

const setHttpTimeout = (timeout: number, xhr: AjaxXhr): TimeoutId | undefined => {
if (!timeout) {
return undefined;
}

// eslint-disable-next-line no-restricted-globals
return setTimeout(() => {
xhr.customStatus = TIMEOUT;
xhr.abort();
}, timeout);
};

const sendRequest = function (options) {
const xhr: XMLHttpRequest & { customStatus?: string } = httpRequest.getXhr();
// @ts-expect-error only void function can be called with new
const d = new Deferred();
const result = d.promise();
// eslint-disable-next-line @typescript-eslint/no-explicit-any
const sendRequest = (options: AjaxRequestOptions): any => {
const xhr: AjaxXhr = httpRequest.getXhr();
const d = Deferred<unknown>();
const result = d.promise() as AjaxRequestResult;
const async = isDefined(options.async) ? options.async : true;
const { dataType } = options;
const timeout = options.timeout || 0;
let timeoutId;
const timeout = options.timeout ?? 0;

options.crossDomain = isCrossDomain(options.url);
const needScriptEvaluation = dataType === 'jsonp' || dataType === 'script';

// eslint-disable-next-line @typescript-eslint/prefer-nullish-coalescing
if (options.cache === undefined) {
options.cache = !needScriptEvaluation;
}
Expand All @@ -92,17 +105,16 @@ const sendRequest = function (options) {
const { parameters } = requestOptions;

if (callbackName) {
// @ts-expect-error window[callback] is window type
window[callbackName] = function (data) {
window[callbackName] = (data: unknown): void => {
d.resolve(data, SUCCESS, xhr);
};
}

if (options.crossDomain && needScriptEvaluation) {
const reject = function () {
const reject = (): void => {
d.reject(xhr, ERROR);
};
const resolve = function () {
const resolve = (): void => {
if (dataType === 'jsonp') return;
d.resolve(null, SUCCESS, xhr);
};
Expand All @@ -126,11 +138,11 @@ const sendRequest = function (options) {

if (async) {
xhr.timeout = timeout;
timeoutId = setHttpTimeout(timeout, xhr);
}

// eslint-disable-next-line @typescript-eslint/no-unused-vars
xhr.onreadystatechange = function (e) {
const timeoutId = async ? setHttpTimeout(timeout, xhr) : undefined;

xhr.onreadystatechange = (): void => {
if (xhr.readyState === 4) {
clearTimeout(timeoutId);
if (isStatusSuccess(xhr.status)) {
Expand All @@ -140,40 +152,40 @@ const sendRequest = function (options) {
d.resolve(null, NO_CONTENT, xhr);
}
} else {
d.reject(xhr, xhr.customStatus || ERROR);
d.reject(xhr, xhr.customStatus ?? ERROR);
}
}
};

if (options.upload) {
xhr.upload.onprogress = options.upload.onprogress;
xhr.upload.onloadstart = options.upload.onloadstart;
xhr.upload.onabort = options.upload.onabort;
xhr.upload.onprogress = options.upload.onprogress ?? null;
xhr.upload.onloadstart = options.upload.onloadstart ?? null;
xhr.upload.onabort = options.upload.onabort ?? null;
}

if (options.xhrFields) {
for (const field in options.xhrFields) {
xhr[field] = options.xhrFields[field];
}
Object.entries(options.xhrFields).forEach(([field, value]) => {
(xhr as unknown as Record<string, unknown>)[field] = value;
});
}

if (options.responseType === 'arraybuffer') {
xhr.responseType = options.responseType;
}

for (const name in headers) {
if (Object.prototype.hasOwnProperty.call(headers, name) && isDefined(headers[name])) {
xhr.setRequestHeader(name, headers[name]);
Object.entries(headers).forEach(([name, value]) => {
if (isDefined(value)) {
xhr.setRequestHeader(name, value as string);
}
}
});

if (options.beforeSend) {
options.beforeSend(xhr);
}

xhr.send(parameters);
xhr.send(parameters as XMLHttpRequestBodyInit | null);

result.abort = function () {
result.abort = (): void => {
xhr.abort();
};

Expand Down
Loading
Loading