chore: add network logging (#4327)

This commit is contained in:
Wenzhao Hu
2024-12-18 17:28:42 +08:00
committed by GitHub
parent 2a9bdd3ad9
commit a446c82e1f
5 changed files with 92 additions and 48 deletions
@@ -21,10 +21,12 @@ 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.
@@ -32,10 +34,14 @@ import { HTTPProgress, HTTPResponse, HTTPResponseError } from '../response';
* 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).then(() => {}, (error) => {
this._send(request, subscriber, abortController).catch((error) => {
subscriber.error(new HTTPResponseError({
error,
request,
@@ -50,21 +56,28 @@ export class FetchHTTPImplementation implements IHTTPImplementation {
let response: Response;
try {
const fetchParams = this._parseFetchParamsFromRequest(request);
const fetchPromise = fetch(request.getUrlWithParams(), {
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) {
subscriber.error(new HTTPResponseError({
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;
}
@@ -87,13 +100,17 @@ export class FetchHTTPImplementation implements IHTTPImplementation {
statusText,
}));
} else {
subscriber.error(new HTTPResponseError({
const e = new HTTPResponseError({
request,
error: body,
status,
statusText,
headers: responseHeaders,
}));
});
this._logService.error('[FetchHTTPImplementation]: network error', e);
subscriber.error(e);
}
subscriber.complete();
@@ -138,27 +155,21 @@ export class FetchHTTPImplementation implements IHTTPImplementation {
const body = deserialize(request, all, contentType);
return body;
} catch (error) {
subscriber.error(new HTTPResponseError({
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;
}
}
private _parseFetchParamsFromRequest(request: HTTPRequest): RequestInit {
const fetchParams: RequestInit = {
method: request.method,
headers: request.getHeadersInit(),
body: request.getBody(),
credentials: request.withCredentials ? 'include' : undefined,
};
return fetchParams;
}
}
function mergeChunks(chunks: Uint8Array[], totalLength: number): Uint8Array {
@@ -14,11 +14,10 @@
* limitations under the License.
*/
import { createIdentifier } from '@univerjs/core';
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.
@@ -0,0 +1,29 @@
/**
* Copyright 2023-present DreamNum Inc.
*
* 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;
}
@@ -17,38 +17,38 @@
/* 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, type Nullable } 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();
xhr.open(request.method, request.getUrlWithParams());
const urlWithParams = request.getUrlWithParams();
const fetchParams = parseFetchParamsFromRequest(request);
xhr.open(request.method, urlWithParams);
if (request.withCredentials) {
xhr.withCredentials = true;
}
// set default HTTP headers
request.headers.forEach((key, value) => xhr.setRequestHeader(key, value.join(',')));
if (!request.headers.has('Accept')) {
xhr.setRequestHeader('Accept', 'application/json, text/plain, */*');
}
if (!request.headers.has('Content-Type')) {
xhr.setRequestHeader('Content-Type', 'application/json;charset=UTF-8');
if (fetchParams.headers) {
Object.entries(fetchParams.headers).forEach(([key, value]) => xhr.setRequestHeader(key, value));
}
let responseHeader: Nullable<ResponseHeader>;
@@ -103,21 +103,22 @@ export class XHRHTTPImplementation implements IHTTPImplementation {
})
);
} else {
observer.error(
new HTTPResponseError({
request,
error,
headers,
status,
statusText,
})
);
// Handler server logic error here
const e = new HTTPResponseError({
request,
error,
headers,
status,
statusText,
});
this._logService.error('[XHRHTTPImplementation]: network error', e);
observer.error(e);
}
};
const onErrorHandler = (error: ProgressEvent) => {
const res = new HTTPResponseError({
const e = new HTTPResponseError({
request,
error,
status: xhr.status || 0,
@@ -125,7 +126,9 @@ export class XHRHTTPImplementation implements IHTTPImplementation {
headers: buildResponseHeader().headers,
});
observer.error(res);
this._logService.error('[XHRHTTPImplementation]: network error', e);
observer.error(e);
};
xhr.addEventListener('load', onLoadHandler);
@@ -136,6 +139,8 @@ export class XHRHTTPImplementation implements IHTTPImplementation {
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) {
@@ -15,9 +15,9 @@
*/
import type { HTTPHeaders } from './headers';
import { ApplicationJSONType } from './headers';
import type { HTTPResponseType } from './http';
import type { HTTPParams } from './params';
import { ApplicationJSONType } from './headers';
export type HTTPRequestMethod = 'GET' | 'POST' | 'PUT' | 'DELETE' | 'PATCH';
@@ -51,7 +51,7 @@ export class HTTPRequest {
readonly method: HTTPRequestMethod,
readonly url: string,
readonly requestParams?: IHTTPRequestParams
) {}
) { }
getUrlWithParams(): string {
const params = this.requestParams?.params?.toString();