1 type ResponseData = Record<any, any>|string;
3 type RequestOptions = {
4 params?: Record<string, string>,
5 headers?: Record<string, string>
8 type FormattedResponse = {
18 export class HttpError extends Error implements FormattedResponse {
28 constructor(response: Response, content: ResponseData) {
29 super(response.statusText);
31 this.headers = response.headers;
32 this.redirected = response.redirected;
33 this.status = response.status;
34 this.statusText = response.statusText;
35 this.url = response.url;
36 this.original = response;
40 export class HttpManager {
43 * Get the content from a fetch response.
44 * Checks the content-type header to determine the format.
46 protected async getResponseContent(response: Response): Promise<ResponseData|null> {
47 if (response.status === 204) {
51 const responseContentType = response.headers.get('Content-Type') || '';
52 const subType = responseContentType.split(';')[0].split('/').pop();
54 if (subType === 'javascript' || subType === 'json') {
55 return response.json();
58 return response.text();
61 createXMLHttpRequest(method: string, url: string, events: Record<string, (e: Event) => void> = {}): XMLHttpRequest {
62 const csrfToken = document.querySelector('meta[name=token]')?.getAttribute('content');
63 const req = new XMLHttpRequest();
65 for (const [eventName, callback] of Object.entries(events)) {
66 req.addEventListener(eventName, callback.bind(req));
69 req.open(method, url);
70 req.withCredentials = true;
71 req.setRequestHeader('X-CSRF-TOKEN', csrfToken || '');
77 * Create a new HTTP request, setting the required CSRF information
78 * to communicate with the back-end. Parses & formats the response.
80 protected async request(url: string, options: RequestOptions & RequestInit = {}): Promise<FormattedResponse> {
83 if (!requestUrl.startsWith('http')) {
84 requestUrl = window.baseUrl(requestUrl);
88 const urlObj = new URL(requestUrl);
89 for (const paramName of Object.keys(options.params)) {
90 const value = options.params[paramName];
91 if (typeof value !== 'undefined' && value !== null) {
92 urlObj.searchParams.set(paramName, value);
95 requestUrl = urlObj.toString();
98 const csrfToken = document.querySelector('meta[name=token]')?.getAttribute('content') || '';
99 const requestOptions: RequestInit = {...options, credentials: 'same-origin'};
100 requestOptions.headers = {
101 ...requestOptions.headers || {},
102 baseURL: window.baseUrl(''),
103 'X-CSRF-TOKEN': csrfToken,
106 const response = await fetch(requestUrl, requestOptions);
107 const content = await this.getResponseContent(response) || '';
108 const returnData: FormattedResponse = {
110 headers: response.headers,
111 redirected: response.redirected,
112 status: response.status,
113 statusText: response.statusText,
119 throw new HttpError(response, content);
126 * Perform a HTTP request to the back-end that includes data in the body.
127 * Parses the body to JSON if an object, setting the correct headers.
129 protected async dataRequest(method: string, url: string, data: Record<string, any>|null): Promise<FormattedResponse> {
130 const options: RequestInit & RequestOptions = {
132 body: data as BodyInit,
135 // Send data as JSON if a plain object
136 if (typeof data === 'object' && !(data instanceof FormData)) {
138 'Content-Type': 'application/json',
139 'X-Requested-With': 'XMLHttpRequest',
141 options.body = JSON.stringify(data);
144 // Ensure FormData instances are sent over POST
145 // Since Laravel does not read multipart/form-data from other types
146 // of request, hence the addition of the magic _method value.
147 if (data instanceof FormData && method !== 'post') {
148 data.append('_method', method);
149 options.method = 'post';
152 return this.request(url, options);
156 * Perform a HTTP GET request.
157 * Can easily pass query parameters as the second parameter.
159 async get(url: string, params: {} = {}): Promise<FormattedResponse> {
160 return this.request(url, {
167 * Perform a HTTP POST request.
169 async post(url: string, data: null|Record<string, any> = null): Promise<FormattedResponse> {
170 return this.dataRequest('POST', url, data);
174 * Perform a HTTP PUT request.
176 async put(url: string, data: null|Record<string, any> = null): Promise<FormattedResponse> {
177 return this.dataRequest('PUT', url, data);
181 * Perform a HTTP PATCH request.
183 async patch(url: string, data: null|Record<string, any> = null): Promise<FormattedResponse> {
184 return this.dataRequest('PATCH', url, data);
188 * Perform a HTTP DELETE request.
190 async delete(url: string, data: null|Record<string, any> = null): Promise<FormattedResponse> {
191 return this.dataRequest('DELETE', url, data);
195 * Parse the response text for an error response to a user
196 * presentable string. Handles a range of errors responses including
197 * validation responses & server response text.
199 protected formatErrorResponseText(text: string): string {
200 const data = text.startsWith('{') ? JSON.parse(text) : {message: text};
205 if (data.message || data.error) {
206 return data.message || data.error;
209 const values = Object.values(data);
210 const isValidation = values.every(val => {
211 return Array.isArray(val) && val.every(x => typeof x === 'string');
215 return values.flat().join(' ');