1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
|
import type { FetchLike, HttpExchangeFixture } from "./types.js";
export type { FetchLike } from "./types.js";
export function recordFetch(
realFetch: FetchLike,
onExchange: (fx: HttpExchangeFixture) => void,
): FetchLike {
return async (input, init) => {
const url = typeof input === "string" ? input : input instanceof URL ? input.href : input.url;
const method = extractMethod(input, init);
const headers = normalizeHeaders(
init?.headers as Record<string, string> | Headers | [string, string][] | undefined,
);
const reqBody =
init?.body != null
? await readBody(
init.body as
| string
| Blob
| ArrayBuffer
| ArrayBufferView
| URLSearchParams
| ReadableStream,
)
: null;
const response = await realFetch(input, init);
const responseClone = response.clone();
const responseBody = await responseClone.text();
const fixture: HttpExchangeFixture = {
request: { method, url, headers, body: reqBody },
response: {
status: response.status,
statusText: response.statusText,
headers: normalizeResponseHeaders(response.headers),
body: responseBody,
},
};
onExchange(fixture);
return response;
};
}
function extractMethod(input: string | URL | Request, init: RequestInit | undefined): string {
if (init?.method) return init.method;
if (typeof input !== "string" && !(input instanceof URL) && "method" in input) {
return input.method;
}
return "GET";
}
function normalizeHeaders(
raw: Headers | Record<string, string> | [string, string][] | undefined,
): Record<string, string> {
if (!raw) return {};
if (raw instanceof Headers) {
const out: Record<string, string> = {};
raw.forEach((v, k) => {
out[k] = v;
});
return out;
}
if (Array.isArray(raw)) {
const out: Record<string, string> = {};
for (const [k, v] of raw) out[k] = v;
return out;
}
return { ...raw };
}
function normalizeResponseHeaders(headers: Headers): Record<string, string> {
const out: Record<string, string> = {};
headers.forEach((v, k) => {
out[k] = v;
});
return out;
}
async function readBody(
body: string | Blob | ArrayBuffer | ArrayBufferView | URLSearchParams | ReadableStream,
): Promise<string | null> {
if (typeof body === "string") return body;
if (body instanceof Blob) return body.text();
if (body instanceof ArrayBuffer) return new TextDecoder().decode(body);
if (ArrayBuffer.isView(body)) return new TextDecoder().decode(body.buffer);
if (body instanceof URLSearchParams) return body.toString();
if (body instanceof ReadableStream) {
const reader = body.getReader();
const chunks: Uint8Array[] = [];
while (true) {
const { done, value } = await reader.read();
if (done) break;
chunks.push(value);
}
return new TextDecoder().decode(concatUint8Arrays(chunks));
}
return null;
}
function concatUint8Arrays(arrays: Uint8Array[]): Uint8Array {
let totalLen = 0;
for (const a of arrays) totalLen += a.byteLength;
const out = new Uint8Array(totalLen);
let offset = 0;
for (const a of arrays) {
out.set(a, offset);
offset += a.byteLength;
}
return out;
}
|