Press n or j to go to the next uncovered block, b, p or k for the previous block.
| 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 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 227 228 229 230 231 232 233 234 235 236 237 238 239 240 241 242 243 244 245 246 247 248 249 | 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 9x 9x 9x 7x 7x 2x 2x 9x 1x 1x 1x 29x 29x 2416x 2416x 2416x 2416x 29x 29x 1x 1x 1x 23x 23x 23x 23x 23x 23x 23x 23x 8x 15x 15x 23x 23x 23x 23x 23x 23x 23x 23x 23x 23x 1x 1x 1x 15x 15x 15x 15x 15x 15x 15x 15x 15x 15x 14x 14x 14x 14x 14x 14x 15x 15x 9x 9x 9x 9x 9x 9x 9x 15x 15x 15x 15x 3x 8x 1x 1x 1x 1x 15x 5x 5x 5x 5x 5x 5x 5x 5x 5x 5x 5x 5x 5x 15x 5x 5x 5x 5x | import { HttpError } from '@/helpers/exceptions';
import { addCapturedResponse, initConsoleApi } from '@/helpers/responseAggregate';
import { Logger } from '@/utils/Logger';
const logger = new Logger('fetch');
/**
* Response type that extends the standard Response with additional properties
* for data and request hash tracking.
* @category Helpers
* @source
*/
export type FetchDecoratorResponse = Response & { data: unknown; requestHash: string };
// Initialize the console API when in aggregate mode
if (typeof __RESPONSE_AGGREGATE__ !== 'undefined' && __RESPONSE_AGGREGATE__) {
initConsoleApi();
}
/** Maximum number of characters of an error response body kept on an {@link HttpError}. */
const MAX_ERROR_BODY_LENGTH = 1000;
/**
* Reads the text of a failed response so callers can see why the server refused the request.
* @param response - The non-OK response (a clone, so the original stays readable)
* @returns The body truncated to `MAX_ERROR_BODY_LENGTH` characters, or `undefined` if unreadable
* @example
* ```typescript
* await readErrorBody(new Response('INVALID_API_KEY', { status: 400 })); // 'INVALID_API_KEY'
* ```
* @source
*/
async function readErrorBody(response: Response): Promise<string | undefined> {
try {
const text = await response.clone().text();
return text.slice(0, MAX_ERROR_BODY_LENGTH);
} catch {
return undefined;
}
}
/**
* Generates a simple hash from a string using the djb2 algorithm.
* This is a non-cryptographic hash function suitable for request identification.
*
* @category Helpers
* @param str - The string to hash
* @returns A hexadecimal string representing the hash
* @example
* ```typescript
* const hash = generateSimpleHash("test string");
* console.log(hash); // "a5d7d2a9"
* ```
* @source
*/
export function generateSimpleHash(str: string): string {
let hash = 0;
for (let i = 0; i < str.length; i++) {
const char = str.charCodeAt(i);
hash = (hash << 5) - hash + char;
hash = hash & hash; // Convert to 32bit integer
}
return Math.abs(hash).toString(16);
}
/**
* Generates a unique hash for a request based on its URL, method, headers, and body.
* This hash can be used to identify and track unique requests.
*
* @category Helpers
* @param input - The request URL or Request object
* @param options - Optional request configuration
* @returns A promise that resolves to a unique hash string
* @example
* ```typescript
* // With URL string
* const hash1 = await generateRequestHash("https://api.example.com/data", {
* method: "POST",
* headers: { "Content-Type": "application/json" },
* body: JSON.stringify({ name: "test" })
* });
*
* // With Request object
* const request = new Request("https://api.example.com/data", {
* method: "POST",
* headers: { "Content-Type": "application/json" },
* body: JSON.stringify({ name: "test" })
* });
* const hash2 = await generateRequestHash(request);
* ```
* @source
*/
export async function generateRequestHash(
input: RequestInfo | URL,
options?: RequestInit,
): Promise<string> {
const url = input instanceof Request ? input.url : String(input);
const method = input instanceof Request ? input.method : options?.method || 'GET';
const headers = input instanceof Request ? input.headers : options?.headers || {};
const body = input instanceof Request ? input.body : options?.body || '';
const contentType =
input instanceof Request
? input.headers.get('content-type') || ''
: ((headers instanceof Headers
? headers.get('content-type')
: Reflect.get(headers, 'content-type')) ?? '');
const data = {
url,
method,
headers,
body,
contentType,
};
const dataString = JSON.stringify(data);
return generateSimpleHash(dataString);
}
/**
* A decorator function that wraps the native fetch API with additional features:
* - Automatic response parsing based on content type
* - Request hash generation for tracking
* - Error handling
* - Response cloning to prevent body stream consumption
*
* @category Helpers
* @param input - The request URL or Request object
* @param init - Optional request configuration
* @returns A promise that resolves to a FetchDecoratorResponse
* @throws Error if the response is not ok (status not in 200-299 range)
* @example
* ```typescript
* // 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
* }
* ```
* @source
*/
export async function fetchDecorator(
input: RequestInfo | URL,
init?: RequestInit,
): Promise<FetchDecoratorResponse> {
const requestHash = await generateRequestHash(input, init);
logger.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(String(input), 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,
await readErrorBody(clonedResponse),
);
}
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 {
logger.debug('Could not read response as a blob; falling back to text', { 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);
}
// Safe: `data` and `requestHash` were just attached via Object.defineProperties
// above, but the type system can't track those runtime additions.
return enhancedResponse as FetchDecoratorResponse;
}
|