Possibility to pass in what httpModule to use in node HttpStack
omBratteng opened this issue · 3 comments
Is your feature request related to a problem? Please describe.
I am developing an Electron app that is using tus-js-client for uploads, which has been working fine for ages. But then we got the request of supporting proxies, which should've been easy.
But what I have discovered, is that because NodeHttpStack
is importing node:http
and node:https
, the proxy settings set in Electron, somehow does not get passed down.
Describe the solution you'd like
If I could import NodeHttpStack
and initialise it with e.g. new NodeHttpStack({}, net)
then it'd be passed down to Request
and used instead of const httpModule = options.protocol === 'https:' ? https : http
Describe alternatives you've considered
Right now I am copying out the upstream NodeHttpStack
just to replace the httpModule
with net
from electron. It works, but I will have to keep an eye with the upstream version, and copy over any changes if there are some.
Can you provide help with implementing this feature?
I could probably open a PR to implement this
Further investigation uncovers that just replacing http
/https
with net
does not work
Closing this as it needs a custom HttpStack.
import type { ReadStream } from 'fs'
import type { HttpRequest, HttpResponse, HttpStack } from 'tus-js-client'
import type { ClientRequest, IncomingMessage } from 'electron'
import type { TransformCallback } from 'stream'
import { net } from 'electron'
import { Readable, Transform } from 'stream'
import throttle from 'lodash.throttle'
export default class ElectronHttpStack implements HttpStack {
createRequest(method: string, url: string): HttpRequest {
return new Request(method, url)
}
getName(): string {
return 'ElectronHttpStack'
}
}
export class Request implements HttpRequest {
method: string
url: string
progressHandler: (bytesSent: number) => void = () => {}
request: ClientRequest
constructor(method: string, url: string) {
this.method = method
this.url = url
this.request = net.request({
url: this.url,
method: this.method,
})
}
getMethod(): string {
return this.method
}
getURL(): string {
return this.url
}
setHeader(header: string, value: string): void {
this.request.setHeader(header, value)
}
getHeader(header: string): string {
return this.request.getHeader(header)
}
setProgressHandler(progressHandler: (bytesSent: number) => void): void {
this.progressHandler = progressHandler
}
send(body: ReadStream): Promise<HttpResponse> {
return new Promise((resolve, reject) => {
const req = this.request
req.on('response', (res) => {
const resChunks: Buffer[] = []
res.on('data', (data) => {
resChunks.push(data)
})
res.on('end', () => {
const responseText = Buffer.concat(resChunks).toString('utf8')
resolve(new Response(res, responseText))
})
})
req.on('error', (err) => {
reject(err)
})
if (body instanceof Readable) {
// @ts-expect-error ClientRequest is writable, but the types don't reflect that
// See issue https://github.com/electron/electron/issues/22730
body.pipe(new ProgressEmitter(this.progressHandler)).pipe(req)
} else {
req.end()
}
})
}
abort(): Promise<void> {
if (this.request !== null) this.request.abort()
return Promise.resolve()
}
getUnderlyingObject(): ClientRequest {
return this.request
}
}
class Response implements HttpResponse {
response: IncomingMessage
body: string
constructor(response: IncomingMessage, body: string) {
this.response = response
this.body = body
}
getStatus() {
return this.response.statusCode
}
getHeader(header: string) {
const headers = this.response.headers[header.toLowerCase()]
return Array.isArray(headers) ? headers.join(',') : headers
}
getBody() {
return this.body
}
getUnderlyingObject() {
return this.response
}
}
// ProgressEmitter is a simple PassThrough-style transform stream which keeps
// track of the number of bytes which have been piped through it and will
// invoke the `onprogress` function whenever new number are available.
class ProgressEmitter extends Transform {
_position: number
_onprogress: (bytesSent: number) => void
constructor(onprogress: (bytesSent: number) => void) {
super()
// The _onprogress property will be invoked, whenever a chunk is piped
// through this transformer. Since chunks are usually quite small (64kb),
// these calls can occur frequently, especially when you have a good
// connection to the remote server. Therefore, we are throtteling them to
// prevent accessive function calls.
this._onprogress = throttle(onprogress, 100, {
leading: true,
trailing: false,
})
this._position = 0
}
_transform(chunk: Buffer, _: BufferEncoding, callback: TransformCallback) {
this._position += chunk.length
this._onprogress(this._position)
callback(null, chunk)
}
}
Wow, thank you very much for sharing your extensive solution!