1- import { AbortSignal } from "abort-controller" ;
21import { URL , URLSearchParams } from "url" ;
32import { Readable } from "stream" ;
4- import { ProxyAgent } from "proxy-agent " ;
3+ import { ProxyAgent , request } from "undici " ;
54import * as retry from "retry" ;
6- import AbortController from "abort-controller" ;
7- import fetch , { HeadersInit , Response , RequestInit , Headers } from "node-fetch" ;
85import * as http from "http" ;
96import * as https from "https" ;
107import util from "util" ;
@@ -38,7 +35,6 @@ const GOOG_QUOTA_USER_HEADER = "x-goog-quota-user";
3835
3936// Header for specifying a quota project. See https://cloud.google.com/apis/docs/system-parameters#project-header
4037export const GOOG_USER_PROJECT_HEADER = "x-goog-user-project" ;
41- const GOOGLE_CLOUD_QUOTA_PROJECT = process . env . GOOGLE_CLOUD_QUOTA_PROJECT ;
4238export const CLI_OAUTH_PROJECT_NUMBER = "563584335869" ;
4339
4440export type HttpMethod =
@@ -150,13 +146,16 @@ export async function getAccessToken(): Promise<string> {
150146}
151147
152148function proxyURIFromEnv ( ) : string | undefined {
153- return (
149+ const uri =
154150 process . env . HTTPS_PROXY ||
155151 process . env . https_proxy ||
156152 process . env . HTTP_PROXY ||
157153 process . env . http_proxy ||
158- undefined
159- ) ;
154+ undefined ;
155+ if ( uri === "undefined" || uri === "null" ) {
156+ return undefined ;
157+ }
158+ return uri ;
160159}
161160
162161// Some networks (and recent Node.js security releases) interact badly with
@@ -198,6 +197,12 @@ export type ClientOptions = {
198197 auth ?: boolean ;
199198} ;
200199
200+ interface FetchOptions extends RequestInit {
201+ dispatcher ?: ProxyAgent ;
202+ duplex ?: "half" ;
203+ agent ?: any ;
204+ }
205+
201206export class Client {
202207 constructor ( private opts : ClientOptions ) {
203208 if ( this . opts . auth === undefined ) {
@@ -357,10 +362,10 @@ export class Client {
357362 }
358363 if (
359364 ! reqOptions . ignoreQuotaProject &&
360- GOOGLE_CLOUD_QUOTA_PROJECT &&
361- GOOGLE_CLOUD_QUOTA_PROJECT !== ""
365+ process . env . GOOGLE_CLOUD_QUOTA_PROJECT &&
366+ process . env . GOOGLE_CLOUD_QUOTA_PROJECT !== ""
362367 ) {
363- reqOptions . headers . set ( GOOG_USER_PROJECT_HEADER , GOOGLE_CLOUD_QUOTA_PROJECT ) ;
368+ reqOptions . headers . set ( GOOG_USER_PROJECT_HEADER , process . env . GOOGLE_CLOUD_QUOTA_PROJECT ) ;
364369 }
365370 return reqOptions ;
366371 }
@@ -409,24 +414,19 @@ export class Client {
409414 }
410415 }
411416
412- const fetchOptions : RequestInit = {
417+ const fetchOptions : FetchOptions = {
413418 headers : options . headers ,
414419 method : options . method ,
415420 redirect : options . redirect ,
416- compress : options . compress ,
417421 } ;
418422
419- if ( proxyURIFromEnv ( ) ) {
420- fetchOptions . agent = new ProxyAgent ( ) ;
423+ const proxyURI = proxyURIFromEnv ( ) ;
424+ if ( proxyURI ) {
425+ fetchOptions . dispatcher = new ProxyAgent ( { uri : proxyURI } ) ;
421426 }
422427
423428 if ( options . signal ) {
424- const signal = options . signal as any ;
425- signal . reason = "" ;
426- signal . throwIfAborted = ( ) => {
427- throw new FirebaseError ( "Aborted" ) ;
428- } ;
429- fetchOptions . signal = signal ;
429+ fetchOptions . signal = options . signal ;
430430 }
431431
432432 let reqTimeout : NodeJS . Timeout | undefined ;
@@ -435,12 +435,7 @@ export class Client {
435435 reqTimeout = setTimeout ( ( ) => {
436436 controller . abort ( ) ;
437437 } , options . timeout ) ;
438- const signal = controller . signal as any ;
439- signal . reason = "" ;
440- signal . throwIfAborted = ( ) => {
441- throw new FirebaseError ( "Aborted" ) ;
442- } ;
443- fetchOptions . signal = signal ;
438+ fetchOptions . signal = controller . signal ;
444439 }
445440
446441 // A request can only be safely retried if its body can be sent again.
@@ -449,11 +444,12 @@ export class Client {
449444 // Raw streams cannot be replayed and therefore disable the keep-alive retry.
450445 let bodyReplayable = true ;
451446 if ( typeof options . body === "string" || Buffer . isBuffer ( options . body ) ) {
452- fetchOptions . body = options . body ;
447+ fetchOptions . body = options . body as any ;
453448 } else if ( options . body instanceof FormData ) {
454- fetchOptions . body = options . body . getBuffer ( ) ;
449+ fetchOptions . body = options . body . getBuffer ( ) as any ;
455450 } else if ( isStream ( options . body ) ) {
456- fetchOptions . body = options . body ;
451+ fetchOptions . body = options . body as any ;
452+ fetchOptions . duplex = "half" ;
457453 bodyReplayable = false ;
458454 } else if ( options . body !== undefined ) {
459455 fetchOptions . body = JSON . stringify ( options . body ) ;
@@ -493,9 +489,38 @@ export class Client {
493489 }
494490 this . logRequest ( options ) ;
495491 try {
496- res = await fetch ( fetchURL , fetchOptions ) ;
492+ if ( options . compress === false ) {
493+ const undiciOptions : any = {
494+ method : fetchOptions . method ,
495+ headers : { } ,
496+ decompress : false ,
497+ } ;
498+ if ( fetchOptions . dispatcher ) {
499+ undiciOptions . dispatcher = fetchOptions . dispatcher ;
500+ }
501+ if ( fetchOptions . signal ) {
502+ undiciOptions . signal = fetchOptions . signal ;
503+ }
504+ if ( fetchOptions . body ) {
505+ undiciOptions . body = fetchOptions . body ;
506+ }
507+ if ( fetchOptions . headers ) {
508+ for ( const [ key , value ] of ( fetchOptions . headers as any ) . entries ( ) ) {
509+ undiciOptions . headers [ key ] = value ;
510+ }
511+ }
512+ const undiciRes = await request ( fetchURL , undiciOptions ) ;
513+ res = new UndiciResponseCompat ( undiciRes ) as any ;
514+ } else {
515+ res = await fetch ( fetchURL , fetchOptions ) ;
516+ }
497517 } catch ( thrown : any ) {
498- const err = thrown instanceof Error ? thrown : new Error ( thrown ) ;
518+ const err =
519+ thrown && typeof thrown === "object" && thrown . cause instanceof Error
520+ ? thrown . cause
521+ : thrown instanceof Error
522+ ? thrown
523+ : new Error ( thrown ) ;
499524 logger . debug (
500525 `*** [apiv2] error from fetch(${ fetchURL } , ${ JSON . stringify ( fetchOptions ) } ): ${ err } ` ,
501526 ) ;
@@ -532,7 +557,17 @@ export class Client {
532557 } else if ( options . responseType === "xml" ) {
533558 body = ( await res . text ( ) ) as unknown as ResT ;
534559 } else if ( options . responseType === "stream" ) {
535- body = res . body as unknown as ResT ;
560+ if ( res . body ) {
561+ if ( typeof ( res . body as any ) . getReader === "function" ) {
562+ body = Readable . fromWeb ( res . body as any ) as unknown as ResT ;
563+ } else {
564+ body = res . body as unknown as ResT ;
565+ }
566+ } else {
567+ const emptyStream = new Readable ( ) ;
568+ emptyStream . push ( null ) ;
569+ body = emptyStream as unknown as ResT ;
570+ }
536571 } else {
537572 throw new FirebaseError ( `Unable to interpret response. Please set responseType.` , {
538573 exit : 2 ,
@@ -650,6 +685,34 @@ function isLocalInsecureRequest(urlPrefix: string): boolean {
650685 return u . protocol === "http:" ;
651686}
652687
688+ class UndiciResponseCompat {
689+ readonly status : number ;
690+ readonly headers : Headers ;
691+ readonly body : any ;
692+ readonly ok : boolean ;
693+
694+ constructor ( private undiciRes : any ) {
695+ this . status = undiciRes . statusCode ;
696+ this . ok = undiciRes . statusCode >= 200 && undiciRes . statusCode < 300 ;
697+ this . headers = new Headers ( ) ;
698+ for ( const [ key , value ] of Object . entries ( undiciRes . headers ) ) {
699+ if ( Array . isArray ( value ) ) {
700+ for ( const v of value ) {
701+ this . headers . append ( key , v ) ;
702+ }
703+ } else if ( value !== undefined ) {
704+ this . headers . set ( key , String ( value ) ) ;
705+ }
706+ }
707+ this . body = undiciRes . body ;
708+ }
709+
710+ async text ( ) : Promise < string > {
711+ const { streamToString } = require ( "./utils" ) ;
712+ return streamToString ( this . body ) ;
713+ }
714+ }
715+
653716function bodyToString ( body : unknown ) : string {
654717 if ( isStream ( body ) ) {
655718 // Don't attempt to read any stream type, in case the caller needs it.
0 commit comments