OpenSlides/client/src/app/core/core-services/http.service.ts

258 lines
9.4 KiB
TypeScript

import { HttpClient, HttpErrorResponse, HttpHeaders } from '@angular/common/http';
import { Injectable } from '@angular/core';
import { TranslateService } from '@ngx-translate/core';
import { OpenSlidesStatusService } from './openslides-status.service';
import { formatQueryParams, QueryParams } from '../definitions/query-params';
/**
* Enum for different HTTPMethods
*/
export enum HTTPMethod {
GET = 'get',
POST = 'post',
PUT = 'put',
PATCH = 'patch',
DELETE = 'delete'
}
export interface DetailResponse {
detail: string | string[];
args?: string[];
}
function isDetailResponse(obj: any): obj is DetailResponse {
return (
obj &&
typeof obj === 'object' &&
(typeof obj.detail === 'string' || obj.detail instanceof Array) &&
(!obj.args || obj.args instanceof Array)
);
}
/**
* Service for managing HTTP requests. Allows to send data for every method. Also (TODO) will do generic error handling.
*/
@Injectable({
providedIn: 'root'
})
export class HttpService {
/**
* http headers used by most requests
*/
private defaultHeaders: HttpHeaders;
/**
* Construct a HttpService
*
* Sets the default headers to application/json
*
* @param http The HTTP Client
* @param translate
* @param timeTravel requests are only allowed if history mode is disabled
*/
public constructor(
private http: HttpClient,
private translate: TranslateService,
private OSStatus: OpenSlidesStatusService
) {
this.defaultHeaders = new HttpHeaders().set('Content-Type', 'application/json');
}
/**
* Send the a http request the the given path.
* Optionally accepts a request body.
*
* @param path the target path, usually starting with /rest
* @param method the required HTTP method (i.e get, post, put)
* @param data optional, if sending a data body is required
* @param queryParams optional queryparams to append to the path
* @param customHeader optional custom HTTP header of required
* @param responseType optional response type, default set to json (i.e 'arraybuffer')
* @returns a promise containing a generic
*/
private async send<T>(
path: string,
method: HTTPMethod,
data?: any,
queryParams?: QueryParams,
customHeader?: HttpHeaders,
responseType?: string
): Promise<T> {
// end early, if we are in history mode
if (this.OSStatus.isInHistoryMode && method !== HTTPMethod.GET) {
throw this.handleError('You cannot make changes while in history mode');
}
// there is a current bug with the responseType.
// https://github.com/angular/angular/issues/18586
// castting it to 'json' allows the usage of the current array
if (!responseType) {
responseType = 'json';
}
let url = path + formatQueryParams(queryParams);
if (url[0] !== '/') {
console.warn(`Please prefix the URL "${url}" with a slash.`);
url = '/' + url;
}
if (this.OSStatus.isPrioritizedClient) {
url = '/prioritize' + url;
}
const options = {
body: data,
headers: customHeader ? customHeader : this.defaultHeaders,
responseType: responseType as 'json'
};
try {
return await this.http.request<T>(method, url, options).toPromise();
} catch (e) {
throw this.handleError(e);
}
}
/**
* Takes an error thrown by the HttpClient. Processes it to return a string that can
* be presented to the user.
* @param e The error thrown.
* @returns The prepared and translated message for the user
*/
private handleError(e: any): string {
let error = this.translate.instant('Error') + ': ';
// If the error is a string already, return it.
if (typeof e === 'string') {
return error + e;
}
// If the error is no HttpErrorResponse, it's not clear what is wrong.
if (!(e instanceof HttpErrorResponse)) {
console.error('Unknown error thrown by the http client: ', e);
error += this.translate.instant('An unknown error occurred.');
return error;
}
if (e.status === 405) {
// this should only happen, if the url is wrong -> a bug.
error += this.translate.instant(
'The requested method is not allowed. Please contact your system administrator.'
);
} else if (!e.error) {
error += this.translate.instant("The server didn't respond.");
} else if (typeof e.error === 'object') {
if (isDetailResponse(e.error)) {
error += this.processDetailResponse(e.error);
} else {
error = Object.keys(e.error)
.map(key => {
const capitalizedKey = key.charAt(0).toUpperCase() + key.slice(1);
return this.translate.instant(capitalizedKey) + ': ' + this.processDetailResponse(e.error[key]);
})
.join(', ');
}
} else if (e.status === 500) {
error += this.translate.instant('A server error occured. Please contact your system administrator.');
} else if (e.status > 500) {
error += this.translate.instant('The server could not be reached.') + ` (${e.status})`;
} else {
error += e.message;
}
return error;
}
/**
* Errors from the servers may be string or array of strings. This function joins the strings together,
* if an array is send.
* @param str a string or a string array to join together.
* @returns Error text(s) as single string
*/
private processDetailResponse(response: DetailResponse): string {
let message: string;
if (response instanceof Array) {
message = response.join(' ');
} else if (response.detail instanceof Array) {
message = response.detail.join(' ');
} else {
message = response.detail;
}
message = this.translate.instant(message);
if (response.args && response.args.length > 0) {
for (let i = 0; i < response.args.length; i++) {
message = message.replace(`{${i}}`, response.args[i].toString());
}
}
return message;
}
/**
* Executes a get on a path with a certain object
* @param path The path to send the request to.
* @param data An optional payload for the request.
* @param queryParams Optional params appended to the path as the query part of the url.
* @param header optional HTTP header if required
* @param responseType option expected response type by the request (i.e 'arraybuffer')
* @returns A promise holding a generic
*/
public async get<T>(
path: string,
data?: any,
queryParams?: QueryParams,
header?: HttpHeaders,
responseType?: string
): Promise<T> {
return await this.send<T>(path, HTTPMethod.GET, data, queryParams, header, responseType);
}
/**
* Executes a post on a path with a certain object
* @param path The path to send the request to.
* @param data An optional payload for the request.
* @param queryParams Optional params appended to the path as the query part of the url.
* @param header optional HTTP header if required
* @returns A promise holding a generic
*/
public async post<T>(path: string, data?: any, queryParams?: QueryParams, header?: HttpHeaders): Promise<T> {
return await this.send<T>(path, HTTPMethod.POST, data, queryParams, header);
}
/**
* Executes a put on a path with a certain object
* @param path The path to send the request to.
* @param data An optional payload for the request.
* @param queryParams Optional params appended to the path as the query part of the url.
* @param header optional HTTP header if required
* @returns A promise holding a generic
*/
public async patch<T>(path: string, data?: any, queryParams?: QueryParams, header?: HttpHeaders): Promise<T> {
return await this.send<T>(path, HTTPMethod.PATCH, data, queryParams, header);
}
/**
* Executes a put on a path with a certain object
* @param path The path to send the request to.
* @param data An optional payload for the request.
* @param queryParams Optional params appended to the path as the query part of the url.
* @param header optional HTTP header if required
* @returns A promise holding a generic
*/
public async put<T>(path: string, data?: any, queryParams?: QueryParams, header?: HttpHeaders): Promise<T> {
return await this.send<T>(path, HTTPMethod.PUT, data, queryParams, header);
}
/**
* Makes a delete request.
* @param url The path to send the request to.
* @param data An optional payload for the request.
* @param queryParams Optional params appended to the path as the query part of the url.
* @param header optional HTTP header if required
* @returns A promise holding a generic
*/
public async delete<T>(path: string, data?: any, queryParams?: QueryParams, header?: HttpHeaders): Promise<T> {
return await this.send<T>(path, HTTPMethod.DELETE, data, queryParams, header);
}
}