musicbrainz-api
Version:
MusicBrainz API client for reading and submitting metadata
99 lines • 3.86 kB
JavaScript
import Debug from "debug";
const debug = Debug('musicbrainz-api-node');
function isConnectionReset(err) {
// Undici puts the OS error on .cause, with .code like 'ECONNRESET'
const code = err?.cause?.code ?? err?.code;
// Add other transient codes you consider safe to retry:
return typeof code === "string" && code === 'ECONNRESET';
}
export class HttpClient {
constructor(httpOptions) {
this.httpOptions = httpOptions;
}
get(path, options) {
return this._fetch('get', path, options);
}
post(path, options) {
return this._fetch('post', path, options);
}
postForm(path, formData, options) {
const encodedFormData = new URLSearchParams(formData).toString();
return this._fetch('post', path, { ...options, body: encodedFormData, headers: { 'Content-Type': 'application/x-www-form-urlencoded' } });
}
postJson(path, json, options) {
const encodedJson = JSON.stringify(json);
return this._fetch('post', path, { ...options, body: encodedJson, headers: { 'Content-Type': 'application/json.' } });
}
async _fetch(method, path, options) {
if (!options)
options = {};
let retryLimit = options.retryLimit && options.retryLimit > 1 ? options.retryLimit : 1;
const retryTimeout = this.httpOptions.timeout ? this.httpOptions.timeout : 500;
const url = this._buildUrl(path, options.query);
const cookies = await this.getCookies();
const headers = new Headers(options.headers);
headers.set('User-Agent', this.httpOptions.userAgent);
if (cookies !== null) {
headers.set('Cookie', cookies);
}
while (retryLimit > 0) {
let response;
try {
response = await fetch(url, {
method,
...options,
headers,
body: options.body,
redirect: options.followRedirects === false ? 'manual' : 'follow'
});
}
catch (err) {
if (isConnectionReset(err)) {
// Retry on TCP connection resets
await this._delay(retryTimeout); // wait 200ms before retry
continue;
}
throw err;
}
if (response.status === 429 || response.status === 503) {
debug(`Received status=${response.status}, assume reached rate limit, retry in ${retryTimeout} ms`);
retryLimit--;
if (retryLimit > 0) {
await this._delay(retryTimeout); // wait 200ms before retry
continue;
}
}
await this.registerCookies(response);
return response;
}
throw new Error(`Failed to fetch ${url} after retries`);
}
// Helper: Builds URL with query string
_buildUrl(path, query) {
let url = path.startsWith('/')
? `${this.httpOptions.baseUrl}${path}`
: `${this.httpOptions.baseUrl}/${path}`;
if (query) {
const urlSearchParams = new URLSearchParams();
for (const key of Object.keys(query)) {
const value = query[key];
(Array.isArray(value) ? value : [value]).forEach(v => {
urlSearchParams.append(key, v);
});
}
url += `?${urlSearchParams.toString()}`;
}
return url;
}
// Helper: Delays execution
_delay(ms) {
return new Promise(resolve => setTimeout(resolve, ms));
}
registerCookies(_response) {
return Promise.resolve(undefined);
}
async getCookies() {
return Promise.resolve(null);
}
}
//# sourceMappingURL=http-client.js.map