mirror of
https://github.com/RayLabsHQ/gitea-mirror.git
synced 2025-12-25 00:48:06 +03:00
246 lines
6.4 KiB
TypeScript
246 lines
6.4 KiB
TypeScript
/**
|
|
* HTTP client utility functions using fetch() for consistent error handling
|
|
*/
|
|
|
|
export interface HttpResponse<T = any> {
|
|
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<T = any>(
|
|
url: string,
|
|
options: RequestInit = {}
|
|
): Promise<HttpResponse<T>> {
|
|
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) {
|
|
// Try to parse as JSON for better error messages
|
|
try {
|
|
const errorData = JSON.parse(responseText);
|
|
if (errorData.message) {
|
|
errorMessage = `HTTP ${response.status}: ${errorData.message}`;
|
|
} else {
|
|
errorMessage += ` - ${responseText}`;
|
|
}
|
|
} catch {
|
|
// Not JSON, use as-is
|
|
errorMessage += ` - ${responseText}`;
|
|
}
|
|
}
|
|
} catch {
|
|
// Ignore text parsing errors
|
|
}
|
|
|
|
// Log authentication-specific errors for debugging
|
|
if (response.status === 401) {
|
|
console.error(`[HTTP Client] Authentication failed for ${url}`);
|
|
console.error(`[HTTP Client] Response: ${responseText}`);
|
|
if (responseText.includes('user does not exist') && responseText.includes('uid: 0')) {
|
|
console.error(`[HTTP Client] Token appears to be invalid or the user account is not properly configured in Gitea`);
|
|
}
|
|
}
|
|
|
|
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
|
|
if (process.env.NODE_ENV !== 'test') {
|
|
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<T = any>(
|
|
url: string,
|
|
headers?: Record<string, string>
|
|
): Promise<HttpResponse<T>> {
|
|
return httpRequest<T>(url, {
|
|
method: 'GET',
|
|
headers,
|
|
});
|
|
}
|
|
|
|
/**
|
|
* POST request
|
|
*/
|
|
export async function httpPost<T = any>(
|
|
url: string,
|
|
body?: any,
|
|
headers?: Record<string, string>
|
|
): Promise<HttpResponse<T>> {
|
|
return httpRequest<T>(url, {
|
|
method: 'POST',
|
|
headers,
|
|
body: body ? JSON.stringify(body) : undefined,
|
|
});
|
|
}
|
|
|
|
/**
|
|
* PUT request
|
|
*/
|
|
export async function httpPut<T = any>(
|
|
url: string,
|
|
body?: any,
|
|
headers?: Record<string, string>
|
|
): Promise<HttpResponse<T>> {
|
|
return httpRequest<T>(url, {
|
|
method: 'PUT',
|
|
headers,
|
|
body: body ? JSON.stringify(body) : undefined,
|
|
});
|
|
}
|
|
|
|
/**
|
|
* PATCH request
|
|
*/
|
|
export async function httpPatch<T = any>(
|
|
url: string,
|
|
body?: any,
|
|
headers?: Record<string, string>
|
|
): Promise<HttpResponse<T>> {
|
|
return httpRequest<T>(url, {
|
|
method: 'PATCH',
|
|
headers,
|
|
body: body ? JSON.stringify(body) : undefined,
|
|
});
|
|
}
|
|
|
|
/**
|
|
* DELETE request
|
|
*/
|
|
export async function httpDelete<T = any>(
|
|
url: string,
|
|
headers?: Record<string, string>
|
|
): Promise<HttpResponse<T>> {
|
|
return httpRequest<T>(url, {
|
|
method: 'DELETE',
|
|
headers,
|
|
});
|
|
}
|
|
|
|
/**
|
|
* Gitea-specific HTTP client with authentication
|
|
*/
|
|
export class GiteaHttpClient {
|
|
constructor(
|
|
private baseUrl: string,
|
|
private token: string
|
|
) {}
|
|
|
|
private getHeaders(additionalHeaders?: Record<string, string>): Record<string, string> {
|
|
return {
|
|
'Authorization': `token ${this.token}`,
|
|
'Content-Type': 'application/json',
|
|
...additionalHeaders,
|
|
};
|
|
}
|
|
|
|
async get<T = any>(endpoint: string): Promise<HttpResponse<T>> {
|
|
return httpGet<T>(`${this.baseUrl}${endpoint}`, this.getHeaders());
|
|
}
|
|
|
|
async post<T = any>(endpoint: string, body?: any): Promise<HttpResponse<T>> {
|
|
return httpPost<T>(`${this.baseUrl}${endpoint}`, body, this.getHeaders());
|
|
}
|
|
|
|
async put<T = any>(endpoint: string, body?: any): Promise<HttpResponse<T>> {
|
|
return httpPut<T>(`${this.baseUrl}${endpoint}`, body, this.getHeaders());
|
|
}
|
|
|
|
async patch<T = any>(endpoint: string, body?: any): Promise<HttpResponse<T>> {
|
|
return httpPatch<T>(`${this.baseUrl}${endpoint}`, body, this.getHeaders());
|
|
}
|
|
|
|
async delete<T = any>(endpoint: string): Promise<HttpResponse<T>> {
|
|
return httpDelete<T>(`${this.baseUrl}${endpoint}`, this.getHeaders());
|
|
}
|
|
}
|