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' ;
2021-11-17 02:42:35 +03:00
import * as util from 'util' ;
2021-09-17 03:48:43 +03:00
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' ;
2022-01-12 04:33:41 +03:00
import { createInstrumentation } from './clientInstrumentation' ;
2022-01-22 22:25:13 +03:00
import { Tracing } from './tracing' ;
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-11-05 18:27:49 +03:00
export class APIRequest implements api . APIRequest {
2021-10-06 04:53:19 +03:00
private _playwright : Playwright ;
2022-01-12 04:33:41 +03:00
readonly _contexts = new Set < APIRequestContext > ( ) ;
// Instrumentation.
_onDidCreateContext ? : ( context : APIRequestContext ) = > Promise < void > ;
_onWillCloseContext ? : ( context : APIRequestContext ) = > Promise < void > ;
2021-10-06 04:53:19 +03:00
constructor ( playwright : Playwright ) {
this . _playwright = playwright ;
}
2021-11-05 18:27:49 +03:00
async newContext ( options : NewContextOptions = { } ) : Promise < APIRequestContext > {
2021-11-20 03:28:11 +03:00
const storageState = typeof options . storageState === 'string' ?
JSON . parse ( await fs . promises . readFile ( options . storageState , 'utf8' ) ) :
options . storageState ;
2022-01-12 04:33:41 +03:00
const context = APIRequestContext . from ( ( await this . _playwright . _channel . newRequest ( {
2021-11-20 03:28:11 +03:00
. . . options ,
extraHTTPHeaders : options.extraHTTPHeaders ? headersObjectToArray ( options . extraHTTPHeaders ) : undefined ,
storageState ,
} ) ) . request ) ;
2022-01-22 22:25:13 +03:00
context . _tracing . _localUtils = this . _playwright . _utils ;
2022-01-12 04:33:41 +03:00
this . _contexts . add ( context ) ;
2022-02-09 19:54:09 +03:00
context . _request = this ;
2022-01-12 04:33:41 +03:00
await this . _onDidCreateContext ? . ( context ) ;
return context ;
2021-10-06 04:53:19 +03:00
}
}
2021-11-18 02:26:01 +03:00
export class APIRequestContext extends ChannelOwner < channels.APIRequestContextChannel > implements api . APIRequestContext {
2022-02-09 19:54:09 +03:00
_request? : APIRequest ;
2022-01-22 22:25:13 +03:00
readonly _tracing : Tracing ;
2022-01-12 04:33:41 +03:00
2021-11-05 18:27:49 +03:00
static from ( channel : channels.APIRequestContextChannel ) : APIRequestContext {
2021-09-15 04:31:35 +03:00
return ( channel as any ) . _object ;
}
2021-09-13 22:43:07 +03:00
2021-11-05 18:27:49 +03:00
constructor ( parent : ChannelOwner , type : string , guid : string , initializer : channels.APIRequestContextInitializer ) {
2022-01-12 04:33:41 +03:00
super ( parent , type , guid , initializer , createInstrumentation ( ) ) ;
2022-01-22 22:25:13 +03:00
this . _tracing = Tracing . from ( initializer . tracing ) ;
2021-09-13 22:43:07 +03:00
}
2021-11-20 03:28:11 +03:00
async dispose ( ) : Promise < void > {
2022-01-12 04:33:41 +03:00
await this . _request ? . _onWillCloseContext ? . ( this ) ;
2021-11-20 03:28:11 +03:00
await this . _channel . dispose ( ) ;
2022-01-12 04:33:41 +03:00
this . _request ? . _contexts . delete ( this ) ;
2021-09-16 00:02:55 +03:00
}
2021-11-05 18:27:49 +03:00
async delete ( url : string , options? : RequestWithBodyOptions ) : Promise < APIResponse > {
2021-10-07 22:42:26 +03:00
return this . fetch ( url , {
. . . options ,
method : 'DELETE' ,
} ) ;
}
2021-11-05 18:27:49 +03:00
async head ( url : string , options? : RequestWithoutBodyOptions ) : Promise < APIResponse > {
2021-10-07 22:42:26 +03:00
return this . fetch ( url , {
. . . options ,
method : 'HEAD' ,
} ) ;
}
2021-11-05 18:27:49 +03:00
async get ( url : string , options? : RequestWithoutBodyOptions ) : Promise < APIResponse > {
2021-10-06 02:36:15 +03:00
return this . fetch ( url , {
2021-09-13 22:43:07 +03:00
. . . options ,
method : 'GET' ,
} ) ;
}
2021-11-05 18:27:49 +03:00
async patch ( url : string , options? : RequestWithBodyOptions ) : Promise < APIResponse > {
2021-10-07 22:42:26 +03:00
return this . fetch ( url , {
. . . options ,
method : 'PATCH' ,
} ) ;
}
2021-11-05 18:27:49 +03:00
async post ( url : string , options? : RequestWithBodyOptions ) : Promise < APIResponse > {
2021-10-06 02:36:15 +03:00
return this . fetch ( url , {
2021-09-13 22:43:07 +03:00
. . . options ,
method : 'POST' ,
} ) ;
}
2021-11-05 18:27:49 +03:00
async put ( url : string , options? : RequestWithBodyOptions ) : Promise < APIResponse > {
2021-10-07 22:42:26 +03:00
return this . fetch ( url , {
. . . options ,
method : 'PUT' ,
} ) ;
}
2021-11-05 18:27:49 +03:00
async fetch ( urlOrRequest : string | api . Request , options : FetchOptions = { } ) : Promise < APIResponse > {
2021-11-20 03:28:11 +03:00
return this . _wrapApiCall ( async ( ) = > {
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 ) {
2021-11-11 22:12:24 +03:00
if ( isString ( options . data ) ) {
if ( isJsonContentType ( headers ) )
jsonData = options . data ;
else
postDataBuffer = Buffer . from ( options . data , 'utf8' ) ;
} else if ( Buffer . isBuffer ( options . data ) ) {
2021-09-17 03:48:43 +03:00
postDataBuffer = options . data ;
2021-11-11 22:12:24 +03:00
} else if ( typeof options . data === 'object' || typeof options . data === 'number' || typeof options . data === 'boolean' ) {
2021-10-01 22:11:33 +03:00
jsonData = options . data ;
2021-11-11 22:12:24 +03:00
} else {
2021-09-17 03:48:43 +03:00
throw new Error ( ` Unexpected 'data' type ` ) ;
2021-11-11 22:12:24 +03:00
}
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 ) ;
2021-11-20 03:28:11 +03:00
const result = await this . _channel . fetch ( {
2021-09-13 22:43:07 +03:00
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
} ) ;
2021-11-20 07:32:29 +03:00
return new APIResponse ( 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 > {
2021-11-20 03:28:11 +03:00
const state = await this . _channel . storageState ( ) ;
if ( options . path ) {
await mkdirIfNeeded ( options . path ) ;
await fs . promises . writeFile ( options . path , JSON . stringify ( state , undefined , 2 ) , 'utf8' ) ;
}
return state ;
2021-10-01 00:14:29 +03:00
}
2021-09-13 22:43:07 +03:00
}
2021-11-05 18:27:49 +03:00
export class APIResponse implements api . APIResponse {
private readonly _initializer : channels.APIResponse ;
2021-09-13 22:43:07 +03:00
private readonly _headers : RawHeaders ;
2021-11-05 18:27:49 +03:00
private readonly _request : APIRequestContext ;
2021-09-13 22:43:07 +03:00
2021-11-05 18:27:49 +03:00
constructor ( context : APIRequestContext , initializer : channels.APIResponse ) {
2021-09-15 04:31:35 +03:00
this . _request = context ;
2021-09-13 22:43:07 +03:00
this . _initializer = initializer ;
this . _headers = new RawHeaders ( this . _initializer . headers ) ;
}
ok ( ) : boolean {
2021-11-10 01:11:42 +03:00
return this . _initializer . status >= 200 && this . _initializer . status <= 299 ;
2021-09-13 22:43:07 +03:00
}
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-11-20 03:28:11 +03:00
try {
const result = await this . _request . _channel . fetchResponseBody ( { fetchUid : this._fetchUid ( ) } ) ;
if ( result . binary === undefined )
throw new Error ( 'Response has been disposed' ) ;
return Buffer . from ( result . binary ! , 'base64' ) ;
} catch ( e ) {
if ( e . message . includes ( 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-11-20 03:28:11 +03:00
await this . _request . _channel . disposeAPIResponse ( { fetchUid : this._fetchUid ( ) } ) ;
2021-09-13 22:43:07 +03:00
}
2021-11-17 02:42:35 +03:00
[ util . inspect . custom ] ( ) {
const headers = this . headersArray ( ) . map ( ( { name , value } ) = > ` ${ name } : ${ value } ` ) ;
return ` APIResponse: ${ this . status ( ) } ${ this . statusText ( ) } \ n ${ headers . join ( '\n' ) } ` ;
}
2021-09-13 22:43:07 +03:00
_fetchUid ( ) : string {
return this . _initializer . fetchUid ;
}
2021-12-01 05:12:19 +03:00
async _fetchLog ( ) : Promise < string [ ] > {
const { log } = await this . _request . _channel . fetchLog ( { fetchUid : this._fetchUid ( ) } ) ;
return log ;
}
2021-09-13 22:43:07 +03:00
}
2021-09-17 03:48:43 +03:00
2021-11-18 05:12:26 +03:00
type ServerFilePayload = NonNullable < channels.FormField [ ' file ' ] > ;
2021-09-17 03:48:43 +03:00
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 ) ,
buffer : buffer.toString ( 'base64' ) ,
} ;
2021-11-11 22:12:24 +03:00
}
function isJsonContentType ( headers? : HeadersArray ) : boolean {
if ( ! headers )
return false ;
for ( const { name , value } of headers ) {
if ( name . toLocaleLowerCase ( ) === 'content-type' )
return value === 'application/json' ;
}
return false ;
2021-09-17 03:48:43 +03:00
}