This repository has no description
Something went wrong. Try again.
123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156// SPDX-License-Identifier: AGPL-3.0-or-later
import {createHttpClient} from '@pkgs/http_client/src/HttpClient';import type {HttpClient, RequestOptions, StreamResponse} from '@pkgs/http_client/src/HttpClientTypes';import {createPublicInternetRequestUrlPolicy} from '@pkgs/http_client/src/PublicInternetRequestUrlPolicy';
const requestUrlPolicy = createPublicInternetRequestUrlPolicy();const client: HttpClient = createHttpClient({ userAgent: 'fluxer-api', requestUrlPolicy,});const redirectScopedClients = new Map<number, HttpClient>();
interface SendRequestOptions { maxRedirects?: number;}
function getHttpClientForRequest(options?: SendRequestOptions): HttpClient { if (!options?.maxRedirects) { return client; } const existingClient = redirectScopedClients.get(options.maxRedirects); if (existingClient) { return existingClient; } const redirectScopedClient = createHttpClient({ userAgent: 'fluxer-api', maxRedirects: options.maxRedirects, requestUrlPolicy, }); redirectScopedClients.set(options.maxRedirects, redirectScopedClient); return redirectScopedClient;}
export async function sendRequest(opts: RequestOptions, options?: SendRequestOptions) { const requestClient = getHttpClientForRequest(options); return requestClient.sendRequest(opts);}
export class ResponseBodyTooLargeError extends Error { constructor( message: string, readonly maxBytes: number, readonly actualBytes: number | null = null, ) { super(message); this.name = 'ResponseBodyTooLargeError'; }}
interface StreamToStringWithLimitOptions { maxBytes: number; headers?: Headers; url?: string; description?: string; signal?: AbortSignal;}
function parseContentLength(value: string | null | undefined): number | null { if (!value) { return null; } const trimmed = value.trim(); if (!/^\d+$/.test(trimmed)) { return null; } const parsed = Number.parseInt(trimmed, 10); return Number.isSafeInteger(parsed) ? parsed : null;}
function createResponseBodyTooLargeError( options: StreamToStringWithLimitOptions, actualBytes: number | null,): ResponseBodyTooLargeError { const description = options.description ?? 'Response body'; const source = options.url ? ` from ${options.url}` : ''; const actual = actualBytes == null ? 'unknown size' : `${actualBytes} bytes`; return new ResponseBodyTooLargeError( `${description}${source} exceeds the ${options.maxBytes}-byte limit (${actual})`, options.maxBytes, actualBytes, );}
export async function streamToBufferWithLimit( stream: StreamResponse['stream'], options: StreamToStringWithLimitOptions,): Promise<Uint8Array> { if (!stream) { return new Uint8Array(0); } const contentLength = parseContentLength(options.headers?.get('content-length')); if (contentLength != null && contentLength > options.maxBytes) { const error = createResponseBodyTooLargeError(options, contentLength); void stream.cancel(error).catch(() => {}); throw error; } const reader = stream.getReader(); const chunks: Array<Uint8Array> = []; let totalSize = 0; const abortError = new DOMException('Stream read aborted', 'AbortError'); const abortReader = () => { void reader.cancel(abortError).catch(() => {}); }; const isAlreadyAborted = options.signal?.aborted === true; if (!isAlreadyAborted) { options.signal?.addEventListener('abort', abortReader, {once: true}); } try { if (isAlreadyAborted) { abortReader(); throw abortError; } while (true) { if (options.signal?.aborted) { throw abortError; } const {done, value} = await reader.read(); if (done) { break; } if (!value) { continue; } totalSize += value.byteLength; if (totalSize > options.maxBytes) { const error = createResponseBodyTooLargeError(options, totalSize); await reader.cancel(error).catch(() => {}); throw error; } chunks.push(value); } } finally { options.signal?.removeEventListener('abort', abortReader); reader.releaseLock(); } if (chunks.length === 1) { return chunks[0]!; } const merged = new Uint8Array(totalSize); let offset = 0; for (const chunk of chunks) { merged.set(chunk, offset); offset += chunk.byteLength; } return merged;}
export async function streamToStringWithLimit( stream: StreamResponse['stream'], options: StreamToStringWithLimitOptions,): Promise<string> { const merged = await streamToBufferWithLimit(stream, options); return new TextDecoder().decode(merged);}