UNPKG

@drfrost/bods-js

Version:

JavaScript client for the UK's Bus Open Data Service (BODS) API

151 lines (150 loc) 4.93 kB
/** * HTTP client error */ export class HttpClientError extends Error { constructor(message, status, response) { super(message); this.status = status; this.response = response; this.name = 'HttpClientError'; } } /** * Base HTTP client for BODS API requests */ export class HttpClient { constructor(config) { this.apiKey = config.apiKey; this.baseUrl = config.baseUrl || 'https://data.bus-data.dft.gov.uk'; this.timeout = config.timeout || 30000; } /** * Build URL with query parameters */ buildUrl(endpoint, params = {}) { const url = new URL(endpoint, this.baseUrl); // Add API key url.searchParams.set('api_key', this.apiKey); // Add other parameters Object.entries(params).forEach(([key, value]) => { if (value !== undefined && value !== null) { if (Array.isArray(value)) { // Handle array parameters (comma-separated) url.searchParams.set(key, value.join(',')); } else if (value instanceof Date) { // Handle date parameters url.searchParams.set(key, value.toISOString()); } else { url.searchParams.set(key, String(value)); } } }); return url.toString(); } /** * Make HTTP request with timeout and error handling */ async request(endpoint, options = {}) { const { params, ...fetchOptions } = options; const url = this.buildUrl(endpoint, params); const controller = new AbortController(); const timeoutId = setTimeout(() => controller.abort(), this.timeout); try { const response = await fetch(url, { ...fetchOptions, signal: controller.signal, headers: { 'Accept': 'application/json', ...fetchOptions.headers, }, }); clearTimeout(timeoutId); if (!response.ok) { let errorData; try { errorData = await response.json(); } catch { errorData = await response.text(); } const message = typeof errorData === 'object' ? errorData.detail : `HTTP ${response.status}: ${response.statusText}`; throw new HttpClientError(message, response.status, errorData); } const contentType = response.headers.get('content-type'); let data; if (contentType?.includes('application/json')) { data = await response.json(); } else if (contentType?.includes('text/xml') || contentType?.includes('application/xml')) { data = await response.text(); } else if (contentType?.includes('application/protobuf') || contentType?.includes('application/octet-stream')) { data = await response.arrayBuffer(); } else { data = await response.text(); } return { data, status: response.status, statusText: response.statusText, headers: response.headers, }; } catch (error) { clearTimeout(timeoutId); if (error instanceof HttpClientError) { throw error; } if (error instanceof Error) { if (error.name === 'AbortError') { throw new HttpClientError('Request timeout', 408); } throw new HttpClientError(error.message, 0); } throw new HttpClientError('Unknown error occurred', 0); } } /** * Perform GET request */ async get(endpoint, params) { return this.request(endpoint, { method: 'GET', params }); } /** * Perform POST request */ async post(endpoint, body, params) { return this.request(endpoint, { method: 'POST', params, body: JSON.stringify(body), headers: { 'Content-Type': 'application/json', }, }); } /** * Perform PUT request */ async put(endpoint, body, params) { return this.request(endpoint, { method: 'PUT', params, body: JSON.stringify(body), headers: { 'Content-Type': 'application/json', }, }); } /** * Perform DELETE request */ async delete(endpoint, params) { return this.request(endpoint, { method: 'DELETE', params }); } }