UNPKG

wikibase-edit

Version:

Edit Wikibase from NodeJS

122 lines (106 loc) 3.75 kB
import { stringifyQuery, wait, type Query } from '../utils.js' import checkKnownIssues from './check_known_issues.js' import { customFetch, type HttpHeaders, type HttpMethod, type HttpRequestAgent } from './fetch.js' import { getSignatureHeaders } from './oauth.js' import { parseResponseBody } from './parse_response_body.js' import type { PostData } from './post.js' import type { AbsoluteUrl } from '../types/common.js' import type { OAuthCredentials } from '../types/config.js' const timeout = 30000 export interface RequestParams { url: AbsoluteUrl body?: Query | PostData oauth?: OAuthCredentials['oauth'] headers?: HttpHeaders autoRetry?: boolean httpRequestAgent?: HttpRequestAgent } export async function request (method: HttpMethod, params: RequestParams) { method ??= 'get' const { url, body, oauth: oauthTokens, headers, autoRetry = true, httpRequestAgent } = params let maxlag if (typeof body === 'object' && 'maxlag' in body) { maxlag = body.maxlag } let attempts = 1 let bodyStr if (method === 'post' && body != null) { bodyStr = stringifyQuery(body) headers['Content-Type'] = 'application/x-www-form-urlencoded' } async function tryRequest () { if (oauthTokens) { const signatureHeaders = getSignatureHeaders({ url, method, data: body, oauthTokens, }) Object.assign(headers, signatureHeaders) } try { const res = await customFetch(url, { method, body: bodyStr, headers, timeout, agent: httpRequestAgent }) return await parseResponseBody(res) } catch (err) { checkKnownIssues(url, err) if (autoRetry === false) throw err if (errorIsWorthARetry(err)) { const delaySeconds = getRetryDelay(err.headers) * attempts retryWarn(method, url, err, delaySeconds, attempts++, maxlag) await wait(delaySeconds * 1000) return tryRequest() } else { err.context ??= {} err.context.request = { url, body } throw err } } } return tryRequest() } export interface APIResponseError { code: string info: string } function errorIsWorthARetry (err) { if (errorsWorthARetry.has(err.name) || errorsWorthARetry.has(err.type) || errorsCodeWorthARetry.has(err.code)) return true // failed-save might be a recoverable error from the server // See https://github.com/maxlath/wikibase-cli/issues/150 if (err.name === 'failed-save') { const { messages } = err.body.error return !messages.some(isNonRecoverableFailedSave) } if (err.cause) return errorIsWorthARetry(err.cause) return false } const isNonRecoverableFailedSave = message => message.name.startsWith('wikibase-validator') || nonRecoverableFailedSaveMessageNames.has(message.name) const errorsWorthARetry = new Set([ 'maxlag', 'TimeoutError', 'request-timeout', 'wrong response format', ]) const errorsCodeWorthARetry = new Set([ 'ECONNREFUSED', 'ENETUNREACH', 'ENOTFOUND', 'ETIMEDOUT', 'UND_ERR_CONNECT_TIMEOUT', ]) const nonRecoverableFailedSaveMessageNames = new Set([ 'protectedpagetext', 'permissionserrors', ]) const defaultRetryDelay = 5 function getRetryDelay (headers) { const retryAfterSeconds = headers?.['retry-after'] if (/^\d+$/.test(retryAfterSeconds)) return parseInt(retryAfterSeconds) else return defaultRetryDelay } function retryWarn (method, url, err, delaySeconds, attempts, maxlag) { method = method.toUpperCase() const maxlagStr = typeof maxlag === 'number' ? `${maxlag}s` : maxlag console.warn(`[wikibase-edit][WARNING] ${method} ${url} ${err.message}${err.cause ? ` (cause: ${err.cause.message})` : ''} retrying in ${delaySeconds}s (attempt: ${attempts}, maxlag: ${maxlagStr})`) }