2021-09-13 22:43:07 +03:00
/ * *
* Copyright ( c ) Microsoft Corporation .
*
* Licensed under the Apache License , Version 2.0 ( the "License" ) ;
* you may not use this file except in compliance with the License .
* You may obtain a copy of the License at
*
* http : //www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing , software
* distributed under the License is distributed on an "AS IS" BASIS ,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND , either express or implied .
* See the License for the specific language governing permissions and
* limitations under the License .
* /
2021-10-01 00:14:29 +03:00
import fs from 'fs' ;
2021-09-17 03:48:43 +03:00
import path from 'path' ;
import * as mime from 'mime' ;
import { Serializable } from '../../types/structs' ;
2021-09-13 22:43:07 +03:00
import * as api from '../../types/types' ;
import { HeadersArray } from '../common/types' ;
import * as channels from '../protocol/channels' ;
2021-09-16 00:02:55 +03:00
import { kBrowserOrContextClosedError } from '../utils/errors' ;
2021-10-01 00:14:29 +03:00
import { assert , headersObjectToArray , isFilePayload , isString , mkdirIfNeeded , objectToArray } from '../utils/utils' ;
2021-09-15 04:31:35 +03:00
import { ChannelOwner } from './channelOwner' ;
2021-09-13 22:43:07 +03:00
import * as network from './network' ;
import { RawHeaders } from './network' ;
2021-10-01 00:14:29 +03:00
import { FilePayload , Headers , StorageState } from './types' ;
2021-10-06 04:53:19 +03:00
import { Playwright } from './playwright' ;
2021-09-13 22:43:07 +03:00
2021-09-14 00:29:44 +03:00
export type FetchOptions = {
params ? : { [ key : string ] : string ; } ,
method? : string ,
headers? : Headers ,
2021-09-17 03:48:43 +03:00
data? : string | Buffer | Serializable ,
2021-10-01 22:11:33 +03:00
form ? : { [ key : string ] : string | number | boolean ; } ;
multipart ? : { [ key : string ] : string | number | boolean | fs . ReadStream | FilePayload ; } ;
2021-09-14 01:38:27 +03:00
timeout? : number ,
failOnStatusCode? : boolean ,
2021-09-29 01:33:36 +03:00
ignoreHTTPSErrors? : boolean ,
2021-09-14 00:29:44 +03:00
} ;
2021-09-13 22:43:07 +03:00
2021-10-06 04:53:19 +03:00
type NewContextOptions = Omit < channels.PlaywrightNewRequestOptions , ' extraHTTPHeaders ' | ' storageState ' > & {
extraHTTPHeaders? : Headers ,
storageState? : string | StorageState ,
} ;
2021-10-07 22:42:26 +03:00
type RequestWithBodyOptions = Omit < FetchOptions , ' method ' > ;
type RequestWithoutBodyOptions = Omit < RequestWithBodyOptions , ' data ' | ' form ' | ' multipart ' > ;
2021-10-19 17:38:27 +03:00
export class Fetch implements api . APIRequest {
2021-10-06 04:53:19 +03:00
private _playwright : Playwright ;
constructor ( playwright : Playwright ) {
this . _playwright = playwright ;
}
async newContext ( options : NewContextOptions = { } ) : Promise < FetchRequest > {
return await this . _playwright . _wrapApiCall ( async ( channel : channels.PlaywrightChannel ) = > {
const storageState = typeof options . storageState === 'string' ?
JSON . parse ( await fs . promises . readFile ( options . storageState , 'utf8' ) ) :
options . storageState ;
return FetchRequest . from ( ( await channel . newRequest ( {
. . . options ,
extraHTTPHeaders : options.extraHTTPHeaders ? headersObjectToArray ( options . extraHTTPHeaders ) : undefined ,
storageState ,
} ) ) . request ) ;
} ) ;
}
}
2021-10-19 17:38:27 +03:00
export class FetchRequest extends ChannelOwner < channels.FetchRequestChannel , channels.FetchRequestInitializer > implements api . APIRequestContext {
2021-09-15 04:31:35 +03:00
static from ( channel : channels.FetchRequestChannel ) : FetchRequest {
return ( channel as any ) . _object ;
}
2021-09-13 22:43:07 +03:00
2021-09-15 04:31:35 +03:00
constructor ( parent : ChannelOwner , type : string , guid : string , initializer : channels.FetchRequestInitializer ) {
super ( parent , type , guid , initializer ) ;
2021-09-13 22:43:07 +03:00
}
2021-09-16 00:02:55 +03:00
dispose ( ) : Promise < void > {
return this . _wrapApiCall ( async ( channel : channels.FetchRequestChannel ) = > {
await channel . dispose ( ) ;
} ) ;
}
2021-11-03 23:26:15 +03:00
async delete ( url : string , options? : RequestWithBodyOptions ) : Promise < FetchResponse > {
2021-10-07 22:42:26 +03:00
return this . fetch ( url , {
. . . options ,
method : 'DELETE' ,
} ) ;
}
async head ( url : string , options? : RequestWithoutBodyOptions ) : Promise < FetchResponse > {
return this . fetch ( url , {
. . . options ,
method : 'HEAD' ,
} ) ;
}
async get ( url : string , options? : RequestWithoutBodyOptions ) : Promise < FetchResponse > {
2021-10-06 02:36:15 +03:00
return this . fetch ( url , {
2021-09-13 22:43:07 +03:00
. . . options ,
method : 'GET' ,
} ) ;
}
2021-10-07 22:42:26 +03:00
async patch ( url : string , options? : RequestWithBodyOptions ) : Promise < FetchResponse > {
return this . fetch ( url , {
. . . options ,
method : 'PATCH' ,
} ) ;
}
async post ( url : string , options? : RequestWithBodyOptions ) : Promise < FetchResponse > {
2021-10-06 02:36:15 +03:00
return this . fetch ( url , {
2021-09-13 22:43:07 +03:00
. . . options ,
method : 'POST' ,
} ) ;
}
2021-10-07 22:42:26 +03:00
async put ( url : string , options? : RequestWithBodyOptions ) : Promise < FetchResponse > {
return this . fetch ( url , {
. . . options ,
method : 'PUT' ,
} ) ;
}
2021-09-13 22:43:07 +03:00
async fetch ( urlOrRequest : string | api . Request , options : FetchOptions = { } ) : Promise < FetchResponse > {
2021-09-15 04:31:35 +03:00
return this . _wrapApiCall ( async ( channel : channels.FetchRequestChannel ) = > {
2021-09-13 22:43:07 +03:00
const request : network.Request | undefined = ( urlOrRequest instanceof network . Request ) ? urlOrRequest as network.Request : undefined ;
assert ( request || typeof urlOrRequest === 'string' , 'First argument must be either URL string or Request' ) ;
2021-10-01 22:11:33 +03:00
assert ( ( options . data === undefined ? 0 : 1 ) + ( options . form === undefined ? 0 : 1 ) + ( options . multipart === undefined ? 0 : 1 ) <= 1 , ` Only one of 'data', 'form' or 'multipart' can be specified ` ) ;
2021-09-13 22:43:07 +03:00
const url = request ? request . url ( ) : urlOrRequest as string ;
2021-09-14 00:29:44 +03:00
const params = objectToArray ( options . params ) ;
2021-09-13 22:43:07 +03:00
const method = options . method || request ? . method ( ) ;
// Cannot call allHeaders() here as the request may be paused inside route handler.
const headersObj = options . headers || request ? . headers ( ) ;
const headers = headersObj ? headersObjectToArray ( headersObj ) : undefined ;
2021-10-01 22:11:33 +03:00
let jsonData : any ;
let formData : channels.NameValue [ ] | undefined ;
let multipartData : channels.FormField [ ] | undefined ;
2021-09-17 03:48:43 +03:00
let postDataBuffer : Buffer | undefined ;
2021-10-01 22:11:33 +03:00
if ( options . data !== undefined ) {
if ( isString ( options . data ) )
2021-09-17 03:48:43 +03:00
postDataBuffer = Buffer . from ( options . data , 'utf8' ) ;
2021-10-01 22:11:33 +03:00
else if ( Buffer . isBuffer ( options . data ) )
2021-09-17 03:48:43 +03:00
postDataBuffer = options . data ;
2021-10-01 22:11:33 +03:00
else if ( typeof options . data === 'object' )
jsonData = options . data ;
else
2021-09-17 03:48:43 +03:00
throw new Error ( ` Unexpected 'data' type ` ) ;
2021-10-01 22:11:33 +03:00
} else if ( options . form ) {
formData = objectToArray ( options . form ) ;
} else if ( options . multipart ) {
multipartData = [ ] ;
// Convert file-like values to ServerFilePayload structs.
for ( const [ name , value ] of Object . entries ( options . multipart ) ) {
if ( isFilePayload ( value ) ) {
const payload = value as FilePayload ;
if ( ! Buffer . isBuffer ( payload . buffer ) )
throw new Error ( ` Unexpected buffer type of 'data. ${ name } ' ` ) ;
multipartData . push ( { name , file : filePayloadToJson ( payload ) } ) ;
} else if ( value instanceof fs . ReadStream ) {
multipartData . push ( { name , file : await readStreamToJson ( value as fs . ReadStream ) } ) ;
} else {
multipartData . push ( { name , value : String ( value ) } ) ;
}
2021-09-17 03:48:43 +03:00
}
}
2021-10-01 22:11:33 +03:00
if ( postDataBuffer === undefined && jsonData === undefined && formData === undefined && multipartData === undefined )
postDataBuffer = request ? . postDataBuffer ( ) || undefined ;
2021-09-13 22:43:07 +03:00
const postData = ( postDataBuffer ? postDataBuffer . toString ( 'base64' ) : undefined ) ;
const result = await channel . fetch ( {
url ,
2021-09-14 00:29:44 +03:00
params ,
2021-09-13 22:43:07 +03:00
method ,
headers ,
postData ,
2021-10-01 22:11:33 +03:00
jsonData ,
2021-09-17 03:48:43 +03:00
formData ,
2021-10-01 22:11:33 +03:00
multipartData ,
2021-09-13 22:43:07 +03:00
timeout : options.timeout ,
2021-09-14 01:38:27 +03:00
failOnStatusCode : options.failOnStatusCode ,
2021-09-29 01:33:36 +03:00
ignoreHTTPSErrors : options.ignoreHTTPSErrors ,
2021-09-13 22:43:07 +03:00
} ) ;
if ( result . error )
2021-09-17 03:48:43 +03:00
throw new Error ( result . error ) ;
2021-09-15 04:31:35 +03:00
return new FetchResponse ( this , result . response ! ) ;
2021-09-13 22:43:07 +03:00
} ) ;
}
2021-10-01 00:14:29 +03:00
async storageState ( options : { path? : string } = { } ) : Promise < StorageState > {
return await this . _wrapApiCall ( async ( channel : channels.FetchRequestChannel ) = > {
const state = await channel . storageState ( ) ;
if ( options . path ) {
await mkdirIfNeeded ( options . path ) ;
await fs . promises . writeFile ( options . path , JSON . stringify ( state , undefined , 2 ) , 'utf8' ) ;
}
return state ;
} ) ;
}
2021-09-13 22:43:07 +03:00
}
2021-10-19 21:17:23 +03:00
export class FetchResponse implements api . APIResponse {
2021-09-13 22:43:07 +03:00
private readonly _initializer : channels.FetchResponse ;
private readonly _headers : RawHeaders ;
2021-09-15 04:31:35 +03:00
private readonly _request : FetchRequest ;
2021-09-13 22:43:07 +03:00
2021-09-15 04:31:35 +03:00
constructor ( context : FetchRequest , initializer : channels.FetchResponse ) {
this . _request = context ;
2021-09-13 22:43:07 +03:00
this . _initializer = initializer ;
this . _headers = new RawHeaders ( this . _initializer . headers ) ;
}
ok ( ) : boolean {
return this . _initializer . status === 0 || ( this . _initializer . status >= 200 && this . _initializer . status <= 299 ) ;
}
url ( ) : string {
return this . _initializer . url ;
}
status ( ) : number {
return this . _initializer . status ;
}
statusText ( ) : string {
return this . _initializer . statusText ;
}
headers ( ) : Headers {
return this . _headers . headers ( ) ;
}
headersArray ( ) : HeadersArray {
return this . _headers . headersArray ( ) ;
}
async body ( ) : Promise < Buffer > {
2021-09-15 04:31:35 +03:00
return this . _request . _wrapApiCall ( async ( channel : channels.FetchRequestChannel ) = > {
2021-09-16 00:02:55 +03:00
try {
const result = await channel . fetchResponseBody ( { fetchUid : this._fetchUid ( ) } ) ;
2021-10-07 21:38:51 +03:00
if ( result . binary === undefined )
2021-09-16 00:02:55 +03:00
throw new Error ( 'Response has been disposed' ) ;
return Buffer . from ( result . binary ! , 'base64' ) ;
} catch ( e ) {
if ( e . message === kBrowserOrContextClosedError )
throw new Error ( 'Response has been disposed' ) ;
throw e ;
}
2021-09-13 22:43:07 +03:00
} ) ;
}
async text ( ) : Promise < string > {
const content = await this . body ( ) ;
return content . toString ( 'utf8' ) ;
}
async json ( ) : Promise < object > {
const content = await this . text ( ) ;
return JSON . parse ( content ) ;
}
async dispose ( ) : Promise < void > {
2021-09-15 04:31:35 +03:00
return this . _request . _wrapApiCall ( async ( channel : channels.FetchRequestChannel ) = > {
2021-09-13 22:43:07 +03:00
await channel . disposeFetchResponse ( { fetchUid : this._fetchUid ( ) } ) ;
} ) ;
}
_fetchUid ( ) : string {
return this . _initializer . fetchUid ;
}
}
2021-09-17 03:48:43 +03:00
type ServerFilePayload = {
name : string ,
mimeType : string ,
buffer : string ,
} ;
function filePayloadToJson ( payload : FilePayload ) : ServerFilePayload {
return {
name : payload.name ,
mimeType : payload.mimeType ,
buffer : payload.buffer.toString ( 'base64' ) ,
} ;
}
2021-10-01 00:14:29 +03:00
async function readStreamToJson ( stream : fs.ReadStream ) : Promise < ServerFilePayload > {
2021-09-17 03:48:43 +03:00
const buffer = await new Promise < Buffer > ( ( resolve , reject ) = > {
const chunks : Buffer [ ] = [ ] ;
2021-10-02 05:40:47 +03:00
stream . on ( 'data' , chunk = > chunks . push ( chunk as Buffer ) ) ;
2021-09-17 03:48:43 +03:00
stream . on ( 'end' , ( ) = > resolve ( Buffer . concat ( chunks ) ) ) ;
stream . on ( 'error' , err = > reject ( err ) ) ;
} ) ;
const streamPath : string = Buffer . isBuffer ( stream . path ) ? stream . path . toString ( 'utf8' ) : stream . path ;
return {
name : path.basename ( streamPath ) ,
mimeType : mime.getType ( streamPath ) || 'application/octet-stream' ,
buffer : buffer.toString ( 'base64' ) ,
} ;
}