Skip to content

Instantly share code, notes, and snippets.

@ArjixWasTaken
Last active April 18, 2025 16:35
Show Gist options
  • Save ArjixWasTaken/f3b10875d3b87368f43a2e3e9c2d130f to your computer and use it in GitHub Desktop.
Save ArjixWasTaken/f3b10875d3b87368f43a2e3e9c2d130f to your computer and use it in GitHub Desktop.
A wrapper for XMLHttpRequest that imitates the fetch() API, but allows you to set `onprogress` to track the progress of a request!
type Fetch = typeof globalThis.fetch;
type FetchResponse = ReturnType<Fetch>;
type CustomOptions = {
body?: XMLHttpRequestBodyInit | null;
onprogress?: (event: ProgressEvent) => void;
};
const parseHeaders = (rawHeaders: string) => {
const headers = new Headers();
for (let line of rawHeaders.split("\n")) {
line = line.trim();
const colon = line.indexOf(":");
if (colon === -1) continue;
const key = line.slice(0, colon).trim();
const value = line.slice(colon + 1).trim();
headers.append(key, value);
}
return headers;
};
export const fetch = async (req: URL | string, init?: Omit<RequestInit, "body"> & CustomOptions): FetchResponse => {
const { promise, resolve, reject } = Promise.withResolvers<Response>();
const xhr = new XMLHttpRequest();
xhr.addEventListener("load", () => {
const headers = xhr.getAllResponseHeaders();
const body = xhr.response as Blob;
resolve(
new Response(body, {
status: xhr.status,
statusText: xhr.statusText,
headers: parseHeaders(headers),
})
);
});
xhr.addEventListener("error", reject);
xhr.addEventListener("abort", reject);
xhr.addEventListener("timeout", reject);
if (init?.onprogress) {
xhr.addEventListener("progress", init.onprogress);
}
let url: string;
if (typeof req === "string") url = req;
else if (req instanceof URL) url = req.toString();
else throw new TypeError("Invalid URL");
let method = "GET";
if (init?.method) method = init.method;
if (["GET", "HEAD"].includes(method) && init?.body) {
throw new TypeError("Body not allowed for GET or HEAD requests");
}
xhr.open(method, url, true);
xhr.responseType = "blob";
let credentials: "omit" | "same-origin" | "include" = "same-origin";
if (req instanceof Request) credentials = req.credentials;
if (init?.credentials) credentials = init.credentials;
switch (credentials) {
case "include":
case "same-origin": {
xhr.withCredentials = true;
break;
}
case "omit": {
xhr.withCredentials = false;
break;
}
}
const reqHeaders = req instanceof Request ? req.headers : undefined;
for (const [key, value] of Object.entries(init?.headers ?? reqHeaders ?? {})) {
if (value instanceof Array) {
for (const v of value) {
xhr.setRequestHeader(key, v);
}
} else {
xhr.setRequestHeader(key, value);
}
}
xhr.send(init?.body);
return await promise;
};
@ArjixWasTaken
Copy link
Author

await fetch("/", {
    onprogress(event) {
        // do smth
    }
})

@ArjixWasTaken
Copy link
Author

ArjixWasTaken commented Apr 18, 2025

Note: This is not a 1:1 implementation of fetch, as in, it doesn't accept RequestInfo | URL, but just URL | string.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment