/**
* HTTP Client for Obsidian Local REST API
*/
import { getConfig, debugLog } from './config.js';
export interface RequestOptions {
method: 'GET' | 'POST' | 'PUT' | 'DELETE' | 'PATCH';
path: string;
body?: string | object;
headers?: Record<string, string>;
contentType?: string;
accept?: string;
}
export interface ApiResponse<T = unknown> {
success: boolean;
status: number;
data?: T;
error?: string;
}
export interface HealthCheckResult {
healthy: boolean;
authenticated: boolean;
version?: string;
obsidianVersion?: string;
error?: string;
}
/**
* Make an authenticated request to the Obsidian REST API
*/
export async function makeRequest<T = unknown>(
options: RequestOptions
): Promise<ApiResponse<T>> {
const config = getConfig();
const url = `${config.apiUrl}${options.path}`;
debugLog(`Making ${options.method} request to ${options.path}`);
const headers: Record<string, string> = {
'Authorization': `Bearer ${config.apiKey}`,
...options.headers,
};
// Set Content-Type if body is provided
if (options.body !== undefined) {
if (options.contentType) {
headers['Content-Type'] = options.contentType;
} else if (typeof options.body === 'object') {
headers['Content-Type'] = 'application/json';
} else {
headers['Content-Type'] = 'text/markdown';
}
}
// Set Accept header if specified
if (options.accept) {
headers['Accept'] = options.accept;
}
let body: string | undefined;
if (options.body !== undefined) {
if (typeof options.body === 'object') {
body = JSON.stringify(options.body);
} else {
body = options.body;
}
}
try {
const response = await fetch(url, {
method: options.method,
headers,
body,
});
const status = response.status;
debugLog(`Response status: ${status}`);
// Handle empty responses
const contentLength = response.headers.get('content-length');
const hasContent = contentLength && parseInt(contentLength) > 0;
if (!response.ok) {
let errorMessage = `HTTP ${status}`;
if (hasContent) {
try {
const errorData = await response.json();
errorMessage = errorData.message || errorData.error || errorMessage;
} catch {
errorMessage = await response.text() || errorMessage;
}
}
debugLog(`Request failed: ${errorMessage}`);
return {
success: false,
status,
error: errorMessage,
};
}
// Handle successful responses with no content
if (status === 204 || !hasContent) {
debugLog('Request successful (no content)');
return {
success: true,
status,
};
}
// Parse response based on content type
const contentType = response.headers.get('content-type') || '';
let data: T;
if (contentType.includes('application/json')) {
data = await response.json() as T;
} else {
data = await response.text() as T;
}
debugLog('Request successful');
return {
success: true,
status,
data,
};
} catch (error) {
const errorMessage = error instanceof Error ? error.message : 'Unknown error occurred';
debugLog(`Request error: ${errorMessage}`);
return {
success: false,
status: 0,
error: errorMessage,
};
}
}
/**
* Check health of the Obsidian REST API connection
* Calls the root endpoint to verify connectivity and authentication
*/
export async function checkHealth(): Promise<HealthCheckResult> {
const config = getConfig();
debugLog('Performing health check...');
try {
const url = `${config.apiUrl}/`;
const response = await fetch(url, {
method: 'GET',
headers: {
'Authorization': `Bearer ${config.apiKey}`,
'Accept': 'application/json',
},
});
if (!response.ok) {
debugLog(`Health check failed: HTTP ${response.status}`);
return {
healthy: false,
authenticated: false,
error: `HTTP ${response.status}: ${response.statusText}`,
};
}
const data = await response.json() as {
ok?: string;
authenticated?: boolean;
service?: string;
versions?: { self?: string; obsidian?: string };
};
// Consider healthy if we got a successful response with valid data
// The API may return different values for 'ok' field, so we check multiple indicators
const isHealthy = response.ok && (
data.ok === 'OK' ||
data.ok === 'ok' ||
data.service?.includes('Obsidian') ||
data.authenticated === true
);
const result: HealthCheckResult = {
healthy: isHealthy,
authenticated: data.authenticated === true,
version: data.versions?.self,
obsidianVersion: data.versions?.obsidian,
};
debugLog('Health check result:', result);
return result;
} catch (error) {
const errorMessage = error instanceof Error ? error.message : 'Unknown error';
debugLog(`Health check error: ${errorMessage}`);
return {
healthy: false,
authenticated: false,
error: errorMessage,
};
}
}
/**
* URL-encode a file path for use in API paths
*/
export function encodePath(path: string): string {
return encodeURIComponent(path);
}