tus/tus-js-client

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!