service-core/core/client.mjs

167 lines
5.2 KiB
JavaScript
Raw Normal View History

2020-09-07 00:47:53 +00:00
import http from 'http'
import https from 'https'
import stream from 'stream/promises'
2020-09-07 00:47:53 +00:00
import fs from 'fs'
2022-03-10 11:06:17 +00:00
import Util from './util.mjs'
2020-09-07 00:47:53 +00:00
2022-01-11 16:51:15 +00:00
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'))
}
2022-01-11 16:51:15 +00:00
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 : '')))
2022-01-11 16:51:15 +00:00
}
let parsed
try {
parsed = new URL(path)
2022-01-11 16:51:15 +00:00
} catch {
return Promise.reject(new Error('URL was empty or invalid: ' + path))
2020-09-12 20:31:36 +00:00
}
2020-09-07 00:47:53 +00:00
2022-01-11 16:51:15 +00:00
let h = http
2020-09-07 00:47:53 +00:00
if (parsed.protocol === 'https:') {
h = https
}
let req = null
2022-03-10 11:06:17 +00:00
let orgErr = new Error(); return new Promise(function(resolve, reject) {
2020-09-07 00:47:53 +00:00
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
2022-01-11 16:51:15 +00:00
let timedout = false
let timer = setTimeout(function() {
timedout = true
if (req) { req.destroy() }
2022-03-10 11:06:17 +00:00
reject(Util.combineStack(new Error(`Request ${path} timed out after ${timeout}ms`), orgErr))
2022-01-11 16:51:15 +00:00
}, timeout)
req = h.request({
2020-09-07 00:47:53 +00:00
path: parsed.pathname + parsed.search,
port: parsed.port,
method: 'GET',
headers: headers,
2022-01-11 16:51:15 +00:00
timeout: timeout,
2020-09-07 00:47:53 +00:00
hostname: parsed.hostname
}, function(res) {
2022-01-11 16:51:15 +00:00
if (timedout) { return }
clearTimeout(timer)
const ac = new AbortController()
2020-09-07 00:47:53 +00:00
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
2022-03-10 11:06:17 +00:00
reject(Util.combineStack(err, orgErr))
})
// let file = fs.createWriteStream(filePath)
// res.pipe(file)
2020-09-07 00:47:53 +00:00
} else {
res.on('data', function(chunk) {
output += chunk
})
}
res.on('end', function() {
let err = null
2020-09-07 00:47:53 +00:00
if (res.statusCode >= 300 && res.statusCode < 400) {
2020-09-12 20:31:36 +00:00
if (newRedirects > 5) {
err = new Error(`Too many redirects (last one was ${res.headers.location})`)
2020-09-07 00:47:53 +00:00
}
else if (!res.headers.location) {
err = new Error('Redirect returned no path in location header')
2020-09-12 20:31:36 +00:00
}
else if (res.headers.location.startsWith('http')) {
ac.abort()
2022-01-11 16:51:15 +00:00
return resolve(request(config, res.headers.location, filePath, newRedirects, fastRaw))
2020-09-12 20:31:36 +00:00
} else {
ac.abort()
2022-01-11 16:51:15 +00:00
return resolve(request(config, resolveRelative(path, res.headers.location), filePath, newRedirects, fastRaw))
2020-09-12 20:31:36 +00:00
}
2020-09-07 00:47:53 +00:00
} else if (res.statusCode >= 400) {
err = new Error(`HTTP Error ${res.statusCode}: ${output}`)
}
if (err) {
ac.abort()
2022-03-10 11:06:17 +00:00
if (!filePath) return reject(Util.combineStack(err, orgErr))
// Do some cleanup in case we were in the middle of downloading file
return fs.rm(filePath, function() {
2022-03-10 11:30:43 +00:00
reject(Util.combineStack(err, orgErr))
})
}
// 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
})
2020-09-07 00:47:53 +00:00
}
})
})
2022-01-11 16:51:15 +00:00
req.on('error', function(err) {
if (timedout) return
let wrapped = new Error(`Error during request ${path}: ${err.message}`)
wrapped.code = err.code
2022-03-10 11:06:17 +00:00
reject(Util.combineStack(wrapped, orgErr))
2022-01-11 16:51:15 +00:00
})
req.on('timeout', function(err) {
if (timedout) return
2022-03-10 11:06:17 +00:00
reject(Util.combineStack(err, orgErr))
2022-01-11 16:51:15 +00:00
})
2020-09-07 00:47:53 +00:00
req.end()
}).then(function(res) {
if (req) {
req.destroy()
}
2022-01-11 16:51:15 +00:00
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) {
2022-03-10 11:06:17 +00:00
return Promise.reject(Util.combineStack(new Error('Error parsing body, expected JSON but got HTML instead: ' + res.body), orgErr))
}
2022-03-10 11:06:17 +00:00
return Promise.reject(Util.combineStack(new Error(`Error parsing body ${res.body}: ${e.message}`), orgErr))
2022-01-11 16:51:15 +00:00
}
2020-09-07 00:47:53 +00:00
}
}
return res
})
}