2 * @typedef FormattedResponse
3 * @property {Headers} headers
4 * @property {Response} original
5 * @property {Object|String} data
6 * @property {Boolean} redirected
7 * @property {Number} status
8 * @property {string} statusText
9 * @property {string} url
13 * Get the content from a fetch response.
14 * Checks the content-type header to determine the format.
15 * @param {Response} response
16 * @returns {Promise<Object|String>}
18 async function getResponseContent(response) {
19 if (response.status === 204) {
23 const responseContentType = response.headers.get('Content-Type') || '';
24 const subType = responseContentType.split(';')[0].split('/').pop();
26 if (subType === 'javascript' || subType === 'json') {
27 return response.json();
30 return response.text();
33 export class HttpError extends Error {
35 constructor(response, content) {
36 super(response.statusText);
38 this.headers = response.headers;
39 this.redirected = response.redirected;
40 this.status = response.status;
41 this.statusText = response.statusText;
42 this.url = response.url;
43 this.original = response;
49 * @param {String} method
51 * @param {Object} events
52 * @return {XMLHttpRequest}
54 export function createXMLHttpRequest(method, url, events = {}) {
55 const csrfToken = document.querySelector('meta[name=token]').getAttribute('content');
56 const req = new XMLHttpRequest();
58 for (const [eventName, callback] of Object.entries(events)) {
59 req.addEventListener(eventName, callback.bind(req));
62 req.open(method, url);
63 req.withCredentials = true;
64 req.setRequestHeader('X-CSRF-TOKEN', csrfToken);
70 * Create a new HTTP request, setting the required CSRF information
71 * to communicate with the back-end. Parses & formats the response.
73 * @param {Object} options
74 * @returns {Promise<FormattedResponse>}
76 async function request(url, options = {}) {
79 if (!requestUrl.startsWith('http')) {
80 requestUrl = window.baseUrl(requestUrl);
84 const urlObj = new URL(requestUrl);
85 for (const paramName of Object.keys(options.params)) {
86 const value = options.params[paramName];
87 if (typeof value !== 'undefined' && value !== null) {
88 urlObj.searchParams.set(paramName, value);
91 requestUrl = urlObj.toString();
94 const csrfToken = document.querySelector('meta[name=token]').getAttribute('content');
95 const requestOptions = {...options, credentials: 'same-origin'};
96 requestOptions.headers = {
97 ...requestOptions.headers || {},
98 baseURL: window.baseUrl(''),
99 'X-CSRF-TOKEN': csrfToken,
102 const response = await fetch(requestUrl, requestOptions);
103 const content = await getResponseContent(response);
106 headers: response.headers,
107 redirected: response.redirected,
108 status: response.status,
109 statusText: response.statusText,
115 throw new HttpError(response, content);
122 * Perform a HTTP request to the back-end that includes data in the body.
123 * Parses the body to JSON if an object, setting the correct headers.
124 * @param {String} method
125 * @param {String} url
126 * @param {Object} data
127 * @returns {Promise<FormattedResponse>}
129 async function dataRequest(method, url, data = null) {
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 request(url, options);
156 * Perform a HTTP GET request.
157 * Can easily pass query parameters as the second parameter.
158 * @param {String} url
159 * @param {Object} params
160 * @returns {Promise<FormattedResponse>}
162 export async function get(url, params = {}) {
163 return request(url, {
170 * Perform a HTTP POST request.
171 * @param {String} url
172 * @param {Object} data
173 * @returns {Promise<FormattedResponse>}
175 export async function post(url, data = null) {
176 return dataRequest('POST', url, data);
180 * Perform a HTTP PUT request.
181 * @param {String} url
182 * @param {Object} data
183 * @returns {Promise<FormattedResponse>}
185 export async function put(url, data = null) {
186 return dataRequest('PUT', url, data);
190 * Perform a HTTP PATCH request.
191 * @param {String} url
192 * @param {Object} data
193 * @returns {Promise<FormattedResponse>}
195 export async function patch(url, data = null) {
196 return dataRequest('PATCH', url, data);
200 * Perform a HTTP DELETE request.
201 * @param {String} url
202 * @param {Object} data
203 * @returns {Promise<FormattedResponse>}
205 async function performDelete(url, data = null) {
206 return dataRequest('DELETE', url, data);
209 export {performDelete as delete};
212 * Parse the response text for an error response to a user
213 * presentable string. Handles a range of errors responses including
214 * validation responses & server response text.
215 * @param {String} text
218 export function formatErrorResponseText(text) {
219 const data = text.startsWith('{') ? JSON.parse(text) : {message: text};
224 if (data.message || data.error) {
225 return data.message || data.error;
228 const values = Object.values(data);
229 const isValidation = values.every(val => {
230 return Array.isArray(val) || val.every(x => typeof x === 'string');
234 return values.flat().join(' ');