The request URL or Request object
Optionalinit: RequestInitOptional request configuration
A promise that resolves to a FetchDecoratorResponse
Error if the response is not ok (status not in 200-299 range)
// Basic GET request
const response = await fetchDecorator("https://api.example.com/data");
console.log(response.data); // Parsed response data
console.log(response.requestHash); // Unique request hash
// POST request with JSON body
const response = await fetchDecorator("https://api.example.com/data", {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({ name: "test" })
});
// Using Request object
const request = new Request("https://api.example.com/data", {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({ name: "test" })
});
const response = await fetchDecorator(request);
// Handling different response types
const response = await fetchDecorator("https://api.example.com/data");
if (response.headers.get("content-type")?.includes("application/json")) {
// Data is already parsed as JSON
console.log(response.data);
} else if (response.headers.get("content-type")?.includes("text/")) {
// Data is already parsed as text
console.log(response.data);
} else {
// Data is a Blob
const blob = response.data as Blob;
// Handle blob data
}
export async function fetchDecorator(
input: RequestInfo | URL,
init?: RequestInit,
): Promise<FetchDecoratorResponse> {
const requestHash = await generateRequestHash(input, init);
console.debug(`Request Hash: ${requestHash}`);
// Clone the request for aggregate capture BEFORE fetch() consumes it.
// For POST requests, fetch() reads the request body, making it impossible
// to clone afterward.
let aggregateRequestClone: Request | undefined;
if (typeof __RESPONSE_AGGREGATE__ !== "undefined" && __RESPONSE_AGGREGATE__) {
aggregateRequestClone =
input instanceof Request ? input.clone() : new Request(input.toString(), init);
}
const response = await fetch(input, init);
// So we can return the original response
const clonedResponse = response.clone();
// Clone the response for aggregate capture BEFORE the body is transferred
// to enhancedResponse (which makes the original response disturbed).
// Done before the !response.ok check so error responses (4xx, 5xx) are also captured.
let aggregateResponseClone: Response | undefined;
if (typeof __RESPONSE_AGGREGATE__ !== "undefined" && __RESPONSE_AGGREGATE__) {
aggregateResponseClone = response.clone();
}
if (!response.ok) {
// Still capture error responses for mocking in tests
if (aggregateRequestClone && aggregateResponseClone) {
await addCapturedResponse(aggregateRequestClone, aggregateResponseClone);
}
throw new HttpError(clonedResponse.status, clonedResponse.statusText);
}
const contentType = clonedResponse.headers.get("content-type") || "";
let data: unknown;
try {
if (contentType.includes("application/json")) {
data = await clonedResponse.clone().json();
} else if (contentType.includes("text/") || contentType.includes("json-amazonui-streaming")) {
data = await clonedResponse.clone().text();
} else {
data = await clonedResponse.clone().blob();
}
} catch {
console.debug("clonedResponse:", clonedResponse);
data = await clonedResponse.clone().text();
}
// Create a new Response object that inherits all prototype methods
const enhancedResponse = new Response(response.body, {
status: response.status,
statusText: response.statusText,
headers: response.headers,
});
// Add our custom properties
Object.defineProperties(enhancedResponse, {
data: { value: data },
requestHash: { value: requestHash },
});
// Capture response when in aggregate mode.
// Awaited to ensure the capture completes before the clones are GC'd.
if (aggregateRequestClone && aggregateResponseClone) {
await addCapturedResponse(aggregateRequestClone, aggregateResponseClone);
}
return enhancedResponse as FetchDecoratorResponse;
}
A decorator function that wraps the native fetch API with additional features: