Skip to content

Instantly share code, notes, and snippets.

@westc
Last active October 13, 2023 15:10
Show Gist options
  • Save westc/8f66419c328bac89acfd010965b38c8a to your computer and use it in GitHub Desktop.
Save westc/8f66419c328bac89acfd010965b38c8a to your computer and use it in GitHub Desktop.
Sends a request to the specified URL.
/**
* Sends a request to the specified URL.
* @param {string} url
* The URL to which the request will be sent.
* @param {?request__Options=} options
* The various options to set on the request.
* @returns {Promise<XMLHttpRequest>}
* A promise of the corresponding `XMLHttpRequest`.
*/
function request(url, options) {
let {data, headers, method, onDone, onSetup, params, type} = options ?? {};
return new Promise((resolve, reject) => {
const xhr = new XMLHttpRequest();
// Capture when the request is done loading...
xhr.addEventListener("readystatechange", () => {
if(xhr.readyState === XMLHttpRequest.DONE) {
const success = 200 <= xhr.status && xhr.status < 400;
(success ? resolve : reject)(xhr);
onDone && onDone(
success,
xhr.responseXML ?? xhr.responseText ?? xhr.response,
xhr
);
}
});
// If params were specified add them to the URL.
if (params) {
url = url.replace(
/\?([^#]*)|(?=#)|$/,
(_, qs) => {
const usp = new URLSearchParams(qs);
for (const [name, values] of Object.entries(params)) {
for (const value of Array.isArray(values) ? values : [values]) {
usp.append(name, value);
}
}
return `${usp}` && `?${usp}`;
}
);
}
// Open the URL using the specified method (defaults to "GET").
xhr.open(method ?? "GET", url);
// Set the extra headers if they were specified.
if (headers) {
for (const [key, value] of Object.entries(headers)) {
xhr.setRequestHeader(key, value);
}
}
// Set the content type if specified.
if (type) {
// Convert the data for special types: form-data, form-urlencoded and
// JSON.
const [m, isJSON, isFormData] = /^(?:(?:application\/|text\/)?(json)|multipart\/(form-data)|application\/x-www-form-urlencoded)$/i.exec(type) ?? [];
if (m) {
if (isJSON) {
if ('string' === typeof data) {
try {
JSON.parse(data);
}
catch (e) {
data = JSON.stringify(data);
}
}
else {
try {
data = JSON.stringify(data);
} catch(e) {}
}
}
else if (data && 'object' === typeof data && !(data instanceof FormData)) {
const newContainer = isFormData ? new FormData() : new URLSearchParams();
for (const [key, values] of Object.entries(data)) {
for (const value of (Array.isArray(values) ? values : [values])) {
newContainer.append(key, value);
}
}
data = isFormData ? newContainer : newContainer.toString();
// Remove type if it will be automatically determined via the form
// data.
if (isFormData) type = false;
}
}
// Set the type to content-type unless it was removed.
if (type) xhr.setRequestHeader("Content-Type", type);
}
// If there is an onSetup() function defined go ahead and call it.
if (onSetup) onSetup(xhr);
// Send the request.
xhr.send(data ?? undefined);
});
}
/**
* @typedef request__Options
* @property {?(FormData|string|Record<string,string|string[]>)=} data
* The data sent with the request.
* @property {?{[key: string]: string}=} headers
* The headers to set on the request.
* @property {?("GET"|"POST"|"PUT"|"PATCH"|"DELETE"|"HEAD"|"OPTIONS")=} method
* The method to use to make the request.
* @property {?((xhr: XMLHttpRequest) => void)=} onSetup
* The function that is run right before sending the request.
* @property {?((success: boolean, response: *, xhr: XMLHttpRequest) => void)=} onDone
* The function that is run once the request is done (whether or succeeded
* or failed).
* @property {?Record<string,string|string[]>=} params
* The URL parameters to add to any that were already specified in the given
* URL.
* @property {?string=} type
* The value of the "Content-Type" header. This will override the
* "Content-Type" header set in `options.headers` unless it is set to
* "multipart/form-data" in which case the content-type will be automatically
* generated based on the `data` passed in.
*/
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment