wikibase-edit
Version:
Edit Wikibase from NodeJS
122 lines (106 loc) • 3.75 kB
text/typescript
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})`)
}