service-core/core/client.mjs
Jonatan Nilsson 47344c5e7a
Some checks failed
continuous-integration/appveyor/branch AppVeyor build failed
Updated core logic and how stable is calculated.
Fixed some minor bugs.
Will now no longer travel through history but instead stop at last stable version.
2022-02-18 13:32:44 +00:00

166 lines
No EOL
4.9 KiB
JavaScript

import http from 'http'
import https from 'https'
import stream from 'stream/promises'
import fs from 'fs'
function resolveRelative(from, to) {
const resolvedUrl = new URL(to, new URL(from, 'resolve://'));
if (resolvedUrl.protocol === 'resolve:') {
// `from` is a relative URL.
const { pathname, search, hash } = resolvedUrl;
return pathname + search + hash;
}
return resolvedUrl.toString();
}
export function request(config, path, filePath = null, redirects, fastRaw = false) {
if (!config || typeof(config) !== 'object' || Array.isArray(config)) {
return Promise.reject(new Error('Request must be called with config in first parameter'))
}
let newRedirects = (redirects || 0) + 1
if (!path || typeof(path) !== 'string' || !path.startsWith('http')) {
return Promise.reject(new Error('URL was empty or invalid' + (typeof(path) === 'string' ? ': ' + path : '')))
}
let parsed
try {
parsed = new URL(path)
} catch {
return Promise.reject(new Error('URL was empty or invalid: ' + path))
}
let h = http
if (parsed.protocol === 'https:') {
h = https
}
let req = null
return new Promise(function(resolve, reject) {
if (!path) {
return reject(new Error('Request path was empty'))
}
let headers = {
'User-Agent': 'TheThing/service-core',
Accept: 'application/vnd.github.v3+json'
}
if (config.token) {
headers['Authorization'] = `token ${config.token}`
}
let timeout = config.timeout || 10000
let timedout = false
let timer = setTimeout(function() {
timedout = true
if (req) { req.destroy() }
reject(new Error(`Request ${path} timed out after ${timeout}ms`))
}, timeout)
req = h.request({
path: parsed.pathname + parsed.search,
port: parsed.port,
method: 'GET',
headers: headers,
timeout: timeout,
hostname: parsed.hostname
}, function(res) {
if (timedout) { return }
clearTimeout(timer)
const ac = new AbortController()
let output = ''
if (filePath) {
stream.pipeline(res, fs.createWriteStream(filePath), { signal: ac.signal })
.then(function() {
resolve({
statusCode: res.statusCode,
status: res.statusCode,
statusMessage: res.statusMessage,
headers: res.headers,
body: null
})
}, function(err) {
if (err.code === 'ABORT_ERR') return
reject(err)
})
// let file = fs.createWriteStream(filePath)
// res.pipe(file)
} else {
res.on('data', function(chunk) {
output += chunk
})
}
res.on('end', function() {
let err = null
if (res.statusCode >= 300 && res.statusCode < 400) {
if (newRedirects > 5) {
err = new Error(`Too many redirects (last one was ${res.headers.location})`)
}
else if (!res.headers.location) {
err = new Error('Redirect returned no path in location header')
}
else if (res.headers.location.startsWith('http')) {
ac.abort()
return resolve(request(config, res.headers.location, filePath, newRedirects, fastRaw))
} else {
ac.abort()
return resolve(request(config, resolveRelative(path, res.headers.location), filePath, newRedirects, fastRaw))
}
} else if (res.statusCode >= 400) {
err = new Error(`HTTP Error ${res.statusCode}: ${output}`)
}
if (err) {
ac.abort()
if (!filePath) return reject(err)
// Do some cleanup in case we were in the middle of downloading file
return fs.rm(filePath, function() {
reject(err)
})
}
// Let the pipeline do the resolving so it can finish flusing before calling resolve
if (!filePath) {
resolve({
statusCode: res.statusCode,
status: res.statusCode,
statusMessage: res.statusMessage,
headers: res.headers,
body: output
})
}
})
})
req.on('error', function(err) {
if (timedout) return
let wrapped = new Error(`Error during request ${path}: ${err.message}`)
wrapped.code = err.code
reject(wrapped)
})
req.on('timeout', function(err) {
if (timedout) return
reject(err)
})
req.end()
}).then(function(res) {
if (req) {
req.destroy()
}
if (!filePath && !fastRaw) {
if (typeof(res.body) === 'string') {
try {
res.body = JSON.parse(res.body)
} catch(e) {
if (res.body.indexOf('<!DOCTYPE') < 100 || res.body.indexOf('<html') < 100) {
return Promise.reject(new Error('Error parsing body, expected JSON but got HTML instead: ' + res.body))
}
return Promise.reject(new Error(`Error parsing body ${res.body}: ${e.message}`))
}
}
}
return res
})
}