snapshot of https://github.com/dream-num/univer.git (dev, 2026-09-16; blobs > 131072B omitted)
Some checks failed
📦 Build / build (push) Has been cancelled
🧪 CI Test / test shard (1/4) (push) Has been cancelled
🧪 CI Test / test shard (2/4) (push) Has been cancelled
🧪 CI Test / test shard (3/4) (push) Has been cancelled
🧪 CI Test / test shard (4/4) (push) Has been cancelled
🧪 CI Test / test (push) Has been cancelled
🧪 CI Test / eslint (push) Has been cancelled
🧪 CI Test / typecheck (push) Has been cancelled
🔫 Sync to Pro / dispatch-sync (push) Has been cancelled
🔮 Sync Mirror / sync (push) Has been cancelled

This commit is contained in:
upstream-mirror
2026-09-16 05:09:45 +05:00
commit bcf6ef63ad
7118 changed files with 1236854 additions and 0 deletions

View File

@@ -0,0 +1,200 @@
/**
* Copyright 2023-present DreamNum Co., Ltd.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
import { afterEach, describe, expect, it, vi } from 'vitest';
import { HTTPHeaders } from '../../headers';
import { HTTPRequest } from '../../request';
import { FetchHTTPImplementation } from '../fetch';
afterEach(() => {
vi.restoreAllMocks();
});
describe('FetchHTTPImplementation', () => {
function createRequest(responseType: string, extra: Record<string, unknown> = {}) {
return new HTTPRequest('GET', 'https://example.com', {
responseType,
headers: new HTTPHeaders(),
withCredentials: false,
...extra,
} as any);
}
it('should emit HTTPResponse for json response', async () => {
const body = new ReadableStream({
start(controller) {
controller.enqueue(new TextEncoder().encode(JSON.stringify({ ok: true })));
controller.close();
},
});
vi.stubGlobal('fetch', vi.fn(async () => new Response(body, {
status: 200,
headers: {
'content-type': 'application/json',
'content-length': '12',
},
})));
const impl = new FetchHTTPImplementation({ debug: vi.fn(), error: vi.fn() } as any);
const req = new HTTPRequest('GET', 'https://example.com', {
responseType: 'json',
headers: new HTTPHeaders(),
withCredentials: false,
} as any);
const result = await new Promise<any>((resolve, reject) => {
impl.send(req).subscribe({
next: (e) => resolve(e),
error: reject,
});
});
expect(result.status).toBe(200);
expect(result.body).toEqual({ ok: true });
});
it('should emit HTTPProgress when reportProgress enabled for text', async () => {
const body = new ReadableStream({
start(controller) {
controller.enqueue(new TextEncoder().encode('a'));
controller.enqueue(new TextEncoder().encode('b'));
controller.close();
},
});
vi.stubGlobal('fetch', vi.fn(async () => new Response(body, {
status: 200,
headers: { 'content-type': 'text/plain', 'content-length': '2' },
})));
const impl = new FetchHTTPImplementation({ debug: vi.fn(), error: vi.fn() } as any);
const req = new HTTPRequest('GET', 'https://example.com', {
responseType: 'text',
reportProgress: true,
headers: new HTTPHeaders(),
withCredentials: false,
} as any);
const events: any[] = [];
await new Promise<void>((resolve, reject) => {
impl.send(req).subscribe({
next: (e) => events.push(e),
error: reject,
complete: () => resolve(),
});
});
expect(events.some((e) => e.type === 0)).toBe(true);
expect(events.at(-1).body).toBe('ab');
});
it('should deserialize text, blob, and arraybuffer bodies', async () => {
const impl = new FetchHTTPImplementation({ debug: vi.fn(), error: vi.fn() } as any);
vi.stubGlobal('fetch', vi.fn(async () => new Response('plain', {
status: 200,
headers: { 'content-type': 'text/plain' },
})));
await expect(new Promise<any>((resolve, reject) => {
impl.send(createRequest('text')).subscribe({ next: resolve, error: reject });
})).resolves.toMatchObject({ body: 'plain' });
vi.stubGlobal('fetch', vi.fn(async () => new Response('blob-body', {
status: 200,
headers: { 'content-type': 'text/plain' },
})));
const blobResponse = await new Promise<any>((resolve, reject) => {
impl.send(createRequest('blob')).subscribe({ next: resolve, error: reject });
});
expect(blobResponse.body).toBeInstanceOf(Blob);
expect(await blobResponse.body.text()).toBe('blob-body');
vi.stubGlobal('fetch', vi.fn(async () => new Response(new Uint8Array([1, 2, 3]), {
status: 200,
headers: { 'content-type': 'application/octet-stream' },
})));
const arrayBufferResponse = await new Promise<any>((resolve, reject) => {
impl.send(createRequest('arraybuffer')).subscribe({ next: resolve, error: reject });
});
expect(Array.from(new Uint8Array(arrayBufferResponse.body))).toEqual([1, 2, 3]);
});
it('should emit a null body when the response has no stream', async () => {
vi.stubGlobal('fetch', vi.fn(async () => new Response(null, { status: 204, statusText: 'No Content' })));
const impl = new FetchHTTPImplementation({ debug: vi.fn(), error: vi.fn() } as any);
await expect(new Promise<any>((resolve, reject) => {
impl.send(createRequest('json')).subscribe({ next: resolve, error: reject });
})).resolves.toMatchObject({ status: 204, statusText: 'No Content', body: null });
});
it('should emit HTTPResponseError for failed status, bad JSON, unknown response type, and fetch rejection', async () => {
const logService = { debug: vi.fn(), error: vi.fn() };
const impl = new FetchHTTPImplementation(logService as any);
vi.stubGlobal('fetch', vi.fn(async () => new Response(JSON.stringify({ message: 'bad' }), {
status: 500,
statusText: 'Server Error',
headers: { 'content-type': 'application/json' },
})));
await expect(new Promise((resolve, reject) => {
impl.send(createRequest('json')).subscribe({ next: resolve, error: reject });
})).rejects.toMatchObject({ status: 500, statusText: 'Server Error' });
vi.stubGlobal('fetch', vi.fn(async () => new Response('{bad', {
status: 200,
statusText: 'OK',
headers: { 'content-type': 'application/json' },
})));
await expect(new Promise((resolve, reject) => {
impl.send(createRequest('json')).subscribe({ next: resolve, error: reject });
})).rejects.toMatchObject({ status: 200, statusText: 'OK' });
vi.stubGlobal('fetch', vi.fn(async () => new Response('body', {
status: 200,
statusText: 'OK',
})));
await expect(new Promise((resolve, reject) => {
impl.send(createRequest('document')).subscribe({ next: resolve, error: reject });
})).rejects.toMatchObject({ status: 200, statusText: 'OK' });
vi.stubGlobal('fetch', vi.fn(async () => {
throw Object.assign(new Error('offline'), { status: 0, statusText: 'Offline' });
}));
await expect(new Promise((resolve, reject) => {
impl.send(createRequest('json')).subscribe({ next: resolve, error: reject });
})).rejects.toMatchObject({ status: 0, statusText: 'Offline' });
expect(logService.error).toHaveBeenCalled();
});
it('should abort the fetch request when unsubscribed', () => {
let capturedSignal: AbortSignal | undefined;
vi.stubGlobal('fetch', vi.fn((_url: string, init: RequestInit) => {
capturedSignal = init.signal as AbortSignal;
return new Promise(() => {});
}));
const impl = new FetchHTTPImplementation({ debug: vi.fn(), error: vi.fn() } as any);
const subscription = impl.send(createRequest('json')).subscribe();
expect(capturedSignal?.aborted).toBe(false);
subscription.unsubscribe();
expect(capturedSignal?.aborted).toBe(true);
});
});

View File

@@ -0,0 +1,201 @@
/**
* Copyright 2023-present DreamNum Co., Ltd.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
import { afterEach, describe, expect, it, vi } from 'vitest';
import { HTTPHeaders } from '../../headers';
import { HTTPParams } from '../../params';
import { HTTPRequest } from '../../request';
import { XHRHTTPImplementation } from '../xhr';
type Listener = (event: ProgressEvent) => void;
const xhrInstances: FakeXMLHttpRequest[] = [];
class FakeXMLHttpRequest {
DONE = 4;
readyState = 1;
status = 200;
statusText = 'OK';
responseType = '';
response: unknown;
responseText = '';
withCredentials = false;
method = '';
url = '';
sentBody: unknown;
aborted = false;
headers = new Map<string, string>();
removed: Array<[string, Listener]> = [];
private _listeners = new Map<string, Listener[]>();
constructor() {
xhrInstances.push(this);
}
open(method: string, url: string): void {
this.method = method;
this.url = url;
}
setRequestHeader(key: string, value: string): void {
this.headers.set(key, value);
}
getAllResponseHeaders(): string {
return 'Content-Type: application/json\nX-Trace: trace-1';
}
addEventListener(type: string, listener: Listener): void {
const listeners = this._listeners.get(type) ?? [];
listeners.push(listener);
this._listeners.set(type, listeners);
}
removeEventListener(type: string, listener: Listener): void {
this.removed.push([type, listener]);
this._listeners.set(type, (this._listeners.get(type) ?? []).filter((item) => item !== listener));
}
send(body: unknown): void {
this.sentBody = body;
}
abort(): void {
this.aborted = true;
this.readyState = this.DONE;
this.emit('abort');
}
emit(type: string): void {
(this._listeners.get(type) ?? []).forEach((listener) => listener(new ProgressEvent(type)));
}
}
function createRequest(responseType: string, extra: Record<string, unknown> = {}) {
return new HTTPRequest('POST', 'https://example.com/api', {
responseType,
withCredentials: true,
headers: new HTTPHeaders({ 'X-Test': 'yes' }),
params: new HTTPParams({ q: 'search' }),
body: { ok: true },
...extra,
} as any);
}
afterEach(() => {
vi.restoreAllMocks();
xhrInstances.length = 0;
});
describe('XHRHTTPImplementation', () => {
it('sends requests with headers, credentials and JSON body, then parses JSON responses', async () => {
vi.stubGlobal('XMLHttpRequest', FakeXMLHttpRequest as never);
const impl = new XHRHTTPImplementation({ debug: vi.fn(), error: vi.fn() } as any);
const events: any[] = [];
const promise = new Promise<void>((resolve, reject) => {
impl.send(createRequest('json')).subscribe({
next: (event) => {
events.push(event);
resolve();
},
error: reject,
});
});
const xhr = xhrInstances[0];
xhr.responseText = '{"ok":true}';
xhr.emit('load');
await promise;
expect(xhr.method).toBe('POST');
expect(xhr.url).toBe('https://example.com/api?q=search');
expect(xhr.withCredentials).toBe(true);
expect(xhr.headers.get('x-test')).toBe('yes');
expect(xhr.headers.get('content-type')).toBe('application/json;charset=UTF-8');
expect(xhr.sentBody).toBe('{"ok":true}');
expect(events.at(-1).body).toEqual({ ok: true });
});
it('returns null for no-content responses and raw text for text responses', async () => {
vi.stubGlobal('XMLHttpRequest', FakeXMLHttpRequest as never);
const impl = new XHRHTTPImplementation({ debug: vi.fn(), error: vi.fn() } as any);
const noContentPromise = new Promise<any>((resolve, reject) => {
impl.send(createRequest('json')).subscribe({ next: resolve, error: reject });
});
xhrInstances[0].status = 204;
xhrInstances[0].responseText = '{"ignored":true}';
xhrInstances[0].emit('load');
await expect(noContentPromise).resolves.toMatchObject({ body: null });
const textPromise = new Promise<any>((resolve, reject) => {
impl.send(createRequest('text')).subscribe({ next: resolve, error: reject });
});
xhrInstances[1].responseText = 'plain';
xhrInstances[1].emit('load');
await expect(textPromise).resolves.toMatchObject({ body: 'plain' });
});
it('emits response errors for status errors, invalid JSON, blob mismatches, and network failures', async () => {
const logService = { debug: vi.fn(), error: vi.fn() };
vi.stubGlobal('XMLHttpRequest', FakeXMLHttpRequest as never);
const impl = new XHRHTTPImplementation(logService as any);
const statusError = new Promise((resolve, reject) => {
impl.send(createRequest('json')).subscribe({ next: resolve, error: reject });
});
xhrInstances[0].status = 500;
xhrInstances[0].statusText = 'Server Error';
xhrInstances[0].responseText = '{"ok":false}';
xhrInstances[0].emit('load');
await expect(statusError).rejects.toMatchObject({ status: 500, statusText: 'Server Error' });
const jsonError = new Promise((resolve, reject) => {
impl.send(createRequest('json')).subscribe({ next: resolve, error: reject });
});
xhrInstances[1].responseText = '{bad';
xhrInstances[1].emit('load');
await expect(jsonError).rejects.toMatchObject({ status: 200, statusText: 'OK' });
const blobError = new Promise((resolve, reject) => {
impl.send(createRequest('blob')).subscribe({ next: resolve, error: reject });
});
xhrInstances[2].response = 'not-a-blob';
xhrInstances[2].emit('load');
await expect(blobError).rejects.toMatchObject({ status: 200, statusText: 'OK' });
const networkError = new Promise((resolve, reject) => {
impl.send(createRequest('json')).subscribe({ next: resolve, error: reject });
});
xhrInstances[3].status = 0;
xhrInstances[3].statusText = '';
xhrInstances[3].emit('error');
await expect(networkError).rejects.toMatchObject({ status: 0, statusText: 'Unknown Error' });
expect(logService.error).toHaveBeenCalled();
});
it('aborts pending requests and removes event listeners when unsubscribed', () => {
vi.stubGlobal('XMLHttpRequest', FakeXMLHttpRequest as never);
const impl = new XHRHTTPImplementation({ debug: vi.fn(), error: vi.fn() } as any);
const subscription = impl.send(createRequest('json')).subscribe({ error: () => undefined });
const xhr = xhrInstances[0];
subscription.unsubscribe();
expect(xhr.aborted).toBe(true);
expect(xhr.removed.map(([type]) => type)).toEqual(['load', 'error', 'abort', 'timeout']);
});
});

View File

@@ -0,0 +1,202 @@
/**
* Copyright 2023-present DreamNum Co., Ltd.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
/* eslint-disable ts/no-explicit-any */
/* eslint-disable no-case-declarations */
import type { Subscriber } from 'rxjs';
import type { HTTPRequest } from '../request';
import type { HTTPEvent, HTTPResponseBody } from '../response';
import type { IHTTPImplementation } from './implementation';
import { ILogService } from '@univerjs/core';
import { Observable } from 'rxjs';
import { HTTPHeaders } from '../headers';
import { HTTPStatusCode } from '../http';
import { HTTPProgress, HTTPResponse, HTTPResponseError } from '../response';
import { parseFetchParamsFromRequest } from './util';
/**
* An HTTP implementation using Fetch API. This implementation can both run in browser and Node.js.
*
* It does not support streaming response yet (May 12, 2024).
*/
export class FetchHTTPImplementation implements IHTTPImplementation {
constructor(
@ILogService private readonly _logService: ILogService
) { }
send(request: HTTPRequest): Observable<HTTPEvent<any>> {
return new Observable((subscriber) => {
const abortController = new AbortController();
this._send(request, subscriber, abortController).catch((error) => {
subscriber.error(new HTTPResponseError({
error,
request,
}));
});
return () => abortController.abort();
});
}
private async _send(request: HTTPRequest, subscriber: Subscriber<HTTPEvent<any>>, abortController: AbortController) {
let response: Response;
try {
const fetchParams = parseFetchParamsFromRequest(request);
const urlWithParams = request.getUrlWithParams();
const fetchPromise = fetch(urlWithParams, {
signal: abortController.signal,
...fetchParams,
});
this._logService.debug(`[FetchHTTPImplementation]: sending request to url ${urlWithParams} with params ${fetchParams}`);
response = await fetchPromise;
} catch (error: any) {
const e = new HTTPResponseError({
request,
error,
status: error.status ?? 0,
statusText: error.statusText ?? 'Unknown Error',
headers: error.headers,
});
this._logService.error('[FetchHTTPImplementation]: network error', e);
subscriber.error(e);
return;
}
const responseHeaders = new HTTPHeaders(response.headers);
const status = response.status;
const statusText = response.statusText;
let body: HTTPResponseBody = null;
if (response.body) {
body = await this._readBody(request, response, subscriber);
}
const ok = status >= HTTPStatusCode.Ok && status < HTTPStatusCode.MultipleChoices;
if (ok) {
subscriber.next(new HTTPResponse({
body,
headers: responseHeaders,
status,
statusText,
}));
} else {
const e = new HTTPResponseError({
request,
error: body,
status,
statusText,
headers: responseHeaders,
});
this._logService.error('[FetchHTTPImplementation]: network error', e);
subscriber.error(e);
}
subscriber.complete();
}
private async _readBody(
request: HTTPRequest,
response: Response,
subscriber: Subscriber<HTTPEvent<any>>
): Promise<HTTPResponseBody> {
const chunks: Uint8Array[] = [];
const reader = response.body!.getReader();
const contentLength = response.headers.get('content-length');
let receivedLength = 0;
const reportProgress = request.requestParams?.reportProgress;
const responseType = request.responseType;
let partialText: string | undefined;
let decoder: TextDecoder;
while (true) {
const { done, value } = await reader.read();
if (done) break;
chunks.push(value);
receivedLength += value.length;
if (reportProgress && responseType === 'text') {
partialText = (partialText ?? '') + (decoder ??= new TextDecoder()).decode(value, { stream: true });
subscriber.next(new HTTPProgress(
contentLength ? Number.parseInt(contentLength, 10) : undefined,
receivedLength,
partialText
));
}
}
const all = mergeChunks(chunks, receivedLength);
try {
const contentType = response.headers.get('content-type') ?? '';
const body = deserialize(request, all, contentType);
return body;
} catch (error) {
const e = new HTTPResponseError({
request,
error,
status: response.status,
statusText: response.statusText,
headers: new HTTPHeaders(response.headers),
});
this._logService.error('[FetchHTTPImplementation]: network error', e);
subscriber.error(e);
return null;
}
}
}
function mergeChunks(chunks: Uint8Array[], totalLength: number): Uint8Array {
const all = new Uint8Array(totalLength);
let position = 0;
for (const chunk of chunks) {
all.set(chunk, position);
position += chunk.length;
}
return all;
}
const XSSI_PREFIX = /^\)\]\}',?\n/;
function deserialize(request: HTTPRequest, bin: Uint8Array, contentType: string): HTTPResponseBody {
switch (request.responseType) {
case 'json':
const text = new TextDecoder().decode(bin).replace(XSSI_PREFIX, '');
return text === '' ? null : JSON.parse(text);
case 'text':
return new TextDecoder().decode(bin);
case 'blob':
return new Blob([bin.buffer as ArrayBuffer], { type: contentType });
case 'arraybuffer':
return bin.buffer;
default:
throw new Error(`[FetchHTTPImplementation]: unknown response type: ${request.responseType}.`);
}
}

View File

@@ -0,0 +1,33 @@
/**
* Copyright 2023-present DreamNum Co., Ltd.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
import type { Observable } from 'rxjs';
import type { HTTPRequest } from '../request';
import type { HTTPEvent } from '../response';
import { createIdentifier } from '@univerjs/core';
/**
* HTTP service could be implemented differently on platforms.
*/
export interface IHTTPImplementation {
/**
* Send a request. The result would be returned in an observable for possible stream response.
* @param request the request to be sent
*/
// eslint-disable-next-line ts/no-explicit-any
send(request: HTTPRequest): Observable<HTTPEvent<any>>;
}
export const IHTTPImplementation = createIdentifier<IHTTPImplementation>('network.http-implementation');

View File

@@ -0,0 +1,28 @@
/**
* Copyright 2023-present DreamNum Co., Ltd.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
import type { HTTPRequest } from '../request';
export function parseFetchParamsFromRequest(request: HTTPRequest): RequestInit {
const fetchParams: RequestInit = {
method: request.method,
headers: request.getHeadersInit(),
body: request.getBody(),
credentials: request.withCredentials ? 'include' : undefined,
};
return fetchParams;
}

View File

@@ -0,0 +1,167 @@
/**
* Copyright 2023-present DreamNum Co., Ltd.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
/* eslint-disable max-lines-per-function */
/* eslint-disable ts/no-explicit-any */
import type { Nullable } from '@univerjs/core';
import type { Observer } from 'rxjs';
import type { HTTPRequest } from '../request';
import type { HTTPEvent } from '../response';
import type { IHTTPImplementation } from './implementation';
import { ILogService } from '@univerjs/core';
import { Observable } from 'rxjs';
import { HTTPHeaders } from '../headers';
import { ErrorStatusCodeLowerBound, HTTPStatusCode, SuccessStatusCodeLowerBound } from '../http';
import { HTTPResponse, HTTPResponseError, ResponseHeader } from '../response';
import { parseFetchParamsFromRequest } from './util';
/**
* An HTTP implementation using XHR. HTTP service provided by this service could only be async (we do not support sync XHR now).
*/
export class XHRHTTPImplementation implements IHTTPImplementation {
constructor(
@ILogService private readonly _logService: ILogService
) { }
send(request: HTTPRequest): Observable<HTTPEvent<any>> {
return new Observable((observer: Observer<HTTPEvent<any>>) => {
const xhr = new XMLHttpRequest();
const urlWithParams = request.getUrlWithParams();
const fetchParams = parseFetchParamsFromRequest(request);
const { responseType } = request;
xhr.open(request.method, urlWithParams);
if (request.withCredentials) {
xhr.withCredentials = true;
}
if (fetchParams.headers) {
Object.entries(fetchParams.headers).forEach(([key, value]) => xhr.setRequestHeader(key, value));
}
let responseHeader: Nullable<ResponseHeader>;
const buildResponseHeader = () => {
if (responseHeader) {
return responseHeader;
}
const statusText = xhr.statusText || 'OK';
const headers = new HTTPHeaders(xhr.getAllResponseHeaders());
return new ResponseHeader(headers, xhr.status, statusText);
};
const onLoadHandler = () => {
const { headers, statusText, status } = buildResponseHeader();
let body: any = null;
let error: any = null;
if (status !== HTTPStatusCode.NoContent) {
body = typeof xhr.response === 'undefined' ? xhr.responseText : xhr.response;
}
let success = status >= SuccessStatusCodeLowerBound && status < ErrorStatusCodeLowerBound;
// Parse response body according to request's `responseType`.
// However we will not verify whether the response type is
// the same as the request's `responseType`. We will do that
// in the `HTTPService` class because the verification should be
// the same across different implementations.
if (responseType === 'json' && typeof body === 'string') {
const originalBody = body;
try {
body = body ? JSON.parse(body) : null;
} catch (e) {
// make success as false to emit a HTTPResponseError
success = false;
// revert our change to the original body
body = originalBody;
error = e;
}
}
if (responseType === 'blob' && !(body instanceof Blob)) {
// If the responseType is blob, we should convert the response to a Blob object.
// If the response is not a Blob object, we will make success as false to emit a HTTPResponseError.
success = false;
error = new Error('Response is not a Blob object');
}
if (success) {
observer.next(
new HTTPResponse({
body,
headers,
status,
statusText,
})
);
} else {
const e = new HTTPResponseError({
request,
error,
headers,
status,
statusText,
});
this._logService.error('[XHRHTTPImplementation]: network error', e);
observer.error(e);
}
};
const onErrorHandler = (error: ProgressEvent) => {
const e = new HTTPResponseError({
request,
error,
status: xhr.status || 0,
statusText: xhr.statusText || 'Unknown Error',
headers: buildResponseHeader().headers,
});
this._logService.error('[XHRHTTPImplementation]: network error', e);
observer.error(e);
};
xhr.responseType = responseType || '';
xhr.addEventListener('load', onLoadHandler);
xhr.addEventListener('error', onErrorHandler);
xhr.addEventListener('abort', onErrorHandler);
xhr.addEventListener('timeout', onErrorHandler);
const body = request.getBody();
xhr.send(body);
this._logService.debug('[XHRHTTPImplementation]', `sending request to url ${urlWithParams} with params ${fetchParams}`);
// Abort the request if the subscription is disposed before the request completes.
return () => {
if (xhr.readyState !== xhr.DONE) {
xhr.abort();
}
xhr.removeEventListener('load', onLoadHandler);
xhr.removeEventListener('error', onErrorHandler);
xhr.removeEventListener('abort', onErrorHandler);
xhr.removeEventListener('timeout', onErrorHandler);
};
});
}
}