/** * HTTP client utility functions using fetch() for consistent error handling */ export interface HttpResponse { data: T; status: number; statusText: string; headers: Headers; } export class HttpError extends Error { constructor( message: string, public status: number, public statusText: string, public response?: string ) { super(message); this.name = 'HttpError'; } } /** * Enhanced fetch with consistent error handling and JSON parsing */ export async function httpRequest( url: string, options: RequestInit = {} ): Promise> { try { const response = await fetch(url, { ...options, headers: { 'Content-Type': 'application/json', ...options.headers, }, }); // Clone response for error handling const responseClone = response.clone(); if (!response.ok) { let errorMessage = `HTTP ${response.status}: ${response.statusText}`; let responseText = ''; try { responseText = await responseClone.text(); if (responseText) { errorMessage += ` - ${responseText}`; } } catch { // Ignore text parsing errors } throw new HttpError( errorMessage, response.status, response.statusText, responseText ); } // Check content type for JSON responses const contentType = response.headers.get('content-type'); let data: T; if (contentType && contentType.includes('application/json')) { try { data = await response.json(); } catch (jsonError) { const responseText = await responseClone.text(); // Enhanced JSON parsing error logging console.error("=== JSON PARSING ERROR ==="); console.error("URL:", url); console.error("Status:", response.status, response.statusText); console.error("Content-Type:", contentType); console.error("Response length:", responseText.length); console.error("Response preview (first 500 chars):", responseText.substring(0, 500)); console.error("JSON Error:", jsonError instanceof Error ? jsonError.message : String(jsonError)); console.error("========================"); throw new HttpError( `Failed to parse JSON response from ${url}: ${jsonError instanceof Error ? jsonError.message : String(jsonError)}. Response: ${responseText.substring(0, 200)}${responseText.length > 200 ? '...' : ''}`, response.status, response.statusText, responseText ); } } else { // For non-JSON responses, return text as data data = (await response.text()) as unknown as T; } return { data, status: response.status, statusText: response.statusText, headers: response.headers, }; } catch (error) { if (error instanceof HttpError) { throw error; } // Handle network errors, etc. throw new HttpError( `Network error: ${error instanceof Error ? error.message : String(error)}`, 0, 'Network Error' ); } } /** * GET request */ export async function httpGet( url: string, headers?: Record ): Promise> { return httpRequest(url, { method: 'GET', headers, }); } /** * POST request */ export async function httpPost( url: string, body?: any, headers?: Record ): Promise> { return httpRequest(url, { method: 'POST', headers, body: body ? JSON.stringify(body) : undefined, }); } /** * PUT request */ export async function httpPut( url: string, body?: any, headers?: Record ): Promise> { return httpRequest(url, { method: 'PUT', headers, body: body ? JSON.stringify(body) : undefined, }); } /** * DELETE request */ export async function httpDelete( url: string, headers?: Record ): Promise> { return httpRequest(url, { method: 'DELETE', headers, }); } /** * Gitea-specific HTTP client with authentication */ export class GiteaHttpClient { constructor( private baseUrl: string, private token: string ) {} private getHeaders(additionalHeaders?: Record): Record { return { 'Authorization': `token ${this.token}`, 'Content-Type': 'application/json', ...additionalHeaders, }; } async get(endpoint: string): Promise> { return httpGet(`${this.baseUrl}${endpoint}`, this.getHeaders()); } async post(endpoint: string, body?: any): Promise> { return httpPost(`${this.baseUrl}${endpoint}`, body, this.getHeaders()); } async put(endpoint: string, body?: any): Promise> { return httpPut(`${this.baseUrl}${endpoint}`, body, this.getHeaders()); } async delete(endpoint: string): Promise> { return httpDelete(`${this.baseUrl}${endpoint}`, this.getHeaders()); } }