@trim21/gm-fetch
Version:
fetch for UserScript
150 lines (128 loc) • 4.09 kB
text/typescript
export function parseRawHeaders(h: string): Headers {
const s = h.trim();
if (!s) {
return new Headers();
}
const array: [string, string][] = s.split("\r\n").map((value) => {
let s = value.split(":");
return [s[0].trim(), s[1].trim()];
});
return new Headers(array);
}
export function parseGMResponse(req: Request, res: GM.Response<any>): Response {
// workaround TamperMonkey bug(?) where sometimes response is string despite responseType being "blob"
const headers = parseRawHeaders(res.responseHeaders);
const body =
typeof res.response === "string"
? new Blob([res.response], { type: headers.get("Content-Type") || "text/plain" })
: (res.response as Blob);
return new ResImpl(body, {
statusCode: res.status,
statusText: res.statusText,
headers,
finalUrl: res.finalUrl,
redirected: res.finalUrl === req.url,
});
}
interface ResInit {
redirected: boolean;
headers: Headers;
statusCode: number;
statusText: string;
finalUrl: string;
}
class ResImpl implements Response {
private _bodyUsed: boolean;
private readonly rawBody: Blob;
private readonly init: ResInit;
readonly body: ReadableStream<Uint8Array> | null;
readonly headers: Headers;
readonly redirected: boolean;
readonly status: number;
readonly statusText: string;
readonly type: ResponseType;
readonly url: string;
constructor(body: Blob, init: ResInit) {
this.rawBody = body;
this.init = init;
this.body = body.stream();
const { headers, statusCode, statusText, finalUrl, redirected } = init;
this.headers = headers;
this.status = statusCode;
this.statusText = statusText;
this.url = finalUrl;
this.type = "basic";
this.redirected = redirected;
this._bodyUsed = false;
}
get bodyUsed(): boolean {
return this._bodyUsed;
}
get ok(): boolean {
return this.status < 300;
}
arrayBuffer(): Promise<ArrayBuffer> {
if (this.bodyUsed) {
throw new TypeError("Failed to execute 'arrayBuffer' on 'Response': body stream already read");
}
this._bodyUsed = true;
return this.rawBody.arrayBuffer();
}
blob(): Promise<Blob> {
if (this.bodyUsed) {
throw new TypeError("Failed to execute 'blob' on 'Response': body stream already read");
}
this._bodyUsed = true;
// `slice` will use empty string as default value, so need to pass all arguments.
return Promise.resolve(this.rawBody.slice(0, this.rawBody.size, this.rawBody.type));
}
clone(): Response {
if (this.bodyUsed) {
throw new TypeError("Failed to execute 'clone' on 'Response': body stream already read");
}
return new ResImpl(this.rawBody, this.init);
}
formData(): Promise<FormData> {
if (this.bodyUsed) {
throw new TypeError("Failed to execute 'formData' on 'Response': body stream already read");
}
this._bodyUsed = true;
return this.rawBody.text().then(decode);
}
async json(): Promise<any> {
if (this.bodyUsed) {
throw new TypeError("Failed to execute 'json' on 'Response': body stream already read");
}
this._bodyUsed = true;
return JSON.parse(await this.rawBody.text());
}
text(): Promise<string> {
if (this.bodyUsed) {
throw new TypeError("Failed to execute 'text' on 'Response': body stream already read");
}
this._bodyUsed = true;
return this.rawBody.text();
}
async bytes(): Promise<Uint8Array> {
if (this.bodyUsed) {
throw new TypeError("Failed to execute 'bytes' on 'Response': body stream already read");
}
this._bodyUsed = true;
return new Uint8Array(await this.rawBody.arrayBuffer());
}
}
function decode(body: string) {
const form = new FormData();
body
.trim()
.split("&")
.forEach(function (bytes) {
if (bytes) {
const split = bytes.split("=");
const name = split.shift()?.replace(/\+/g, " ");
const value = split.join("=").replace(/\+/g, " ");
form.append(decodeURIComponent(name!), decodeURIComponent(value));
}
});
return form;
}