2021-09-13 12:43:07 -07: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-09-30 14:14:29 -07:00
import fs from 'fs' ;
2021-09-16 17:48:43 -07:00
import path from 'path' ;
2021-11-16 15:42:35 -08:00
import * as util from 'util' ;
2022-04-06 13:57:14 -08:00
import type { Serializable } from '../../types/structs' ;
import type * as api from '../../types/types' ;
2023-01-13 13:50:38 -08:00
import type { HeadersArray , NameValue } from '../common/types' ;
2022-09-20 18:41:51 -07:00
import type * as channels from '@protocol/channels' ;
2022-04-07 13:36:13 -08:00
import { kBrowserOrContextClosedError } from '../common/errors' ;
2023-01-13 13:50:38 -08:00
import { assert , headersObjectToArray , isString } from '../utils' ;
2022-04-07 19:18:22 -08:00
import { mkdirIfNeeded } from '../utils/fileUtils' ;
2021-09-14 18:31:35 -07:00
import { ChannelOwner } from './channelOwner' ;
2021-09-13 12:43:07 -07:00
import { RawHeaders } from './network' ;
2022-04-06 13:57:14 -08:00
import type { FilePayload , Headers , StorageState } from './types' ;
import type { Playwright } from './playwright' ;
2022-01-11 17:33:41 -08:00
import { createInstrumentation } from './clientInstrumentation' ;
2022-01-22 11:25:13 -08:00
import { Tracing } from './tracing' ;
2021-09-13 12:43:07 -07:00
2021-09-13 14:29:44 -07:00
export type FetchOptions = {
params ? : { [ key : string ] : string ; } ,
method? : string ,
headers? : Headers ,
2021-09-16 17:48:43 -07:00
data? : string | Buffer | Serializable ,
2021-10-01 12:11:33 -07:00
form ? : { [ key : string ] : string | number | boolean ; } ;
multipart ? : { [ key : string ] : string | number | boolean | fs . ReadStream | FilePayload ; } ;
2021-09-13 15:38:27 -07:00
timeout? : number ,
failOnStatusCode? : boolean ,
2021-09-28 15:33:36 -07:00
ignoreHTTPSErrors? : boolean ,
2022-09-09 21:14:42 +02:00
maxRedirects? : number ,
2021-09-13 14:29:44 -07:00
} ;
2021-09-13 12:43:07 -07:00
2021-10-05 17:53:19 -08:00
type NewContextOptions = Omit < channels.PlaywrightNewRequestOptions , 'extraHTTPHeaders' | 'storageState' > & {
extraHTTPHeaders? : Headers ,
storageState? : string | StorageState ,
} ;
2021-10-07 12:42:26 -07:00
type RequestWithBodyOptions = Omit < FetchOptions , 'method' > ;
2021-11-05 16:27:49 +01:00
export class APIRequest implements api . APIRequest {
2021-10-05 17:53:19 -08:00
private _playwright : Playwright ;
2022-01-11 17:33:41 -08:00
readonly _contexts = new Set < APIRequestContext > ( ) ;
// Instrumentation.
_onDidCreateContext ? : ( context : APIRequestContext ) = > Promise < void > ;
_onWillCloseContext ? : ( context : APIRequestContext ) = > Promise < void > ;
2021-10-05 17:53:19 -08:00
constructor ( playwright : Playwright ) {
this . _playwright = playwright ;
}
2021-11-05 16:27:49 +01:00
async newContext ( options : NewContextOptions = { } ) : Promise < APIRequestContext > {
2021-11-19 16:28:11 -08:00
const storageState = typeof options . storageState === 'string' ?
JSON . parse ( await fs . promises . readFile ( options . storageState , 'utf8' ) ) :
options . storageState ;
2022-01-11 17:33:41 -08:00
const context = APIRequestContext . from ( ( await this . _playwright . _channel . newRequest ( {
2021-11-19 16:28:11 -08:00
. . . options ,
extraHTTPHeaders : options.extraHTTPHeaders ? headersObjectToArray ( options . extraHTTPHeaders ) : undefined ,
storageState ,
} ) ) . request ) ;
2022-01-11 17:33:41 -08:00
this . _contexts . add ( context ) ;
2022-02-09 08:54:09 -08:00
context . _request = this ;
2022-01-11 17:33:41 -08:00
await this . _onDidCreateContext ? . ( context ) ;
return context ;
2021-10-05 17:53:19 -08:00
}
}
2021-11-17 15:26:01 -08:00
export class APIRequestContext extends ChannelOwner < channels.APIRequestContextChannel > implements api . APIRequestContext {
2022-02-09 08:54:09 -08:00
_request? : APIRequest ;
2022-01-22 11:25:13 -08:00
readonly _tracing : Tracing ;
2022-01-11 17:33:41 -08:00
2021-11-05 16:27:49 +01:00
static from ( channel : channels.APIRequestContextChannel ) : APIRequestContext {
2021-09-14 18:31:35 -07:00
return ( channel as any ) . _object ;
}
2021-09-13 12:43:07 -07:00
2021-11-05 16:27:49 +01:00
constructor ( parent : ChannelOwner , type : string , guid : string , initializer : channels.APIRequestContextInitializer ) {
2022-01-11 17:33:41 -08:00
super ( parent , type , guid , initializer , createInstrumentation ( ) ) ;
2022-01-22 11:25:13 -08:00
this . _tracing = Tracing . from ( initializer . tracing ) ;
2021-09-13 12:43:07 -07:00
}
2021-11-19 16:28:11 -08:00
async dispose ( ) : Promise < void > {
2022-01-11 17:33:41 -08:00
await this . _request ? . _onWillCloseContext ? . ( this ) ;
2021-11-19 16:28:11 -08:00
await this . _channel . dispose ( ) ;
2022-01-11 17:33:41 -08:00
this . _request ? . _contexts . delete ( this ) ;
2021-09-15 14:02:55 -07:00
}
2021-11-05 16:27:49 +01:00
async delete ( url : string , options? : RequestWithBodyOptions ) : Promise < APIResponse > {
2021-10-07 12:42:26 -07:00
return this . fetch ( url , {
. . . options ,
method : 'DELETE' ,
} ) ;
}
2022-09-27 01:28:07 +09:00
async head ( url : string , options? : RequestWithBodyOptions ) : Promise < APIResponse > {
2021-10-07 12:42:26 -07:00
return this . fetch ( url , {
. . . options ,
method : 'HEAD' ,
} ) ;
}
2022-09-27 01:28:07 +09:00
async get ( url : string , options? : RequestWithBodyOptions ) : Promise < APIResponse > {
2021-10-05 16:36:15 -07:00
return this . fetch ( url , {
2021-09-13 12:43:07 -07:00
. . . options ,
method : 'GET' ,
} ) ;
}
2021-11-05 16:27:49 +01:00
async patch ( url : string , options? : RequestWithBodyOptions ) : Promise < APIResponse > {
2021-10-07 12:42:26 -07:00
return this . fetch ( url , {
. . . options ,
method : 'PATCH' ,
} ) ;
}
2021-11-05 16:27:49 +01:00
async post ( url : string , options? : RequestWithBodyOptions ) : Promise < APIResponse > {
2021-10-05 16:36:15 -07:00
return this . fetch ( url , {
2021-09-13 12:43:07 -07:00
. . . options ,
method : 'POST' ,
} ) ;
}
2021-11-05 16:27:49 +01:00
async put ( url : string , options? : RequestWithBodyOptions ) : Promise < APIResponse > {
2021-10-07 12:42:26 -07:00
return this . fetch ( url , {
. . . options ,
method : 'PUT' ,
} ) ;
}
2021-11-05 16:27:49 +01:00
async fetch ( urlOrRequest : string | api . Request , options : FetchOptions = { } ) : Promise < APIResponse > {
2022-11-30 17:26:19 -08:00
const url = isString ( urlOrRequest ) ? urlOrRequest : undefined ;
const request = isString ( urlOrRequest ) ? undefined : urlOrRequest ;
return this . _innerFetch ( { url , request , . . . options } ) ;
}
async _innerFetch ( options : FetchOptions & { url? : string , request? : api.Request } = { } ) : Promise < APIResponse > {
2021-11-19 16:28:11 -08:00
return this . _wrapApiCall ( async ( ) = > {
2022-11-30 17:26:19 -08:00
assert ( options . request || typeof options . url === 'string' , 'First argument must be either URL string or Request' ) ;
2021-10-01 12:11:33 -07: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 ` ) ;
2022-09-09 21:14:42 +02:00
assert ( options . maxRedirects === undefined || options . maxRedirects >= 0 , ` 'maxRedirects' should be greater than or equal to '0' ` ) ;
2022-11-30 17:26:19 -08:00
const url = options . url !== undefined ? options.url : options.request ! . url ( ) ;
2021-09-13 14:29:44 -07:00
const params = objectToArray ( options . params ) ;
2022-11-30 17:26:19 -08:00
const method = options . method || options . request ? . method ( ) ;
2022-09-09 21:14:42 +02:00
const maxRedirects = options . maxRedirects ;
2021-09-13 12:43:07 -07:00
// Cannot call allHeaders() here as the request may be paused inside route handler.
2022-11-30 17:26:19 -08:00
const headersObj = options . headers || options . request ? . headers ( ) ;
2021-09-13 12:43:07 -07:00
const headers = headersObj ? headersObjectToArray ( headersObj ) : undefined ;
2021-10-01 12:11:33 -07:00
let jsonData : any ;
let formData : channels.NameValue [ ] | undefined ;
let multipartData : channels.FormField [ ] | undefined ;
2021-09-16 17:48:43 -07:00
let postDataBuffer : Buffer | undefined ;
2021-10-01 12:11:33 -07:00
if ( options . data !== undefined ) {
2021-11-11 11:12:24 -08: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-16 17:48:43 -07:00
postDataBuffer = options . data ;
2021-11-11 11:12:24 -08:00
} else if ( typeof options . data === 'object' || typeof options . data === 'number' || typeof options . data === 'boolean' ) {
2021-10-01 12:11:33 -07:00
jsonData = options . data ;
2021-11-11 11:12:24 -08:00
} else {
2021-09-16 17:48:43 -07:00
throw new Error ( ` Unexpected 'data' type ` ) ;
2021-11-11 11:12:24 -08:00
}
2021-10-01 12:11:33 -07: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-16 17:48:43 -07:00
}
}
2021-10-01 12:11:33 -07:00
if ( postDataBuffer === undefined && jsonData === undefined && formData === undefined && multipartData === undefined )
2022-11-30 17:26:19 -08:00
postDataBuffer = options . request ? . postDataBuffer ( ) || undefined ;
2023-01-05 14:39:49 -08:00
const fixtures = {
__testHookLookup : ( options as any ) . __testHookLookup
} ;
2021-11-19 16:28:11 -08:00
const result = await this . _channel . fetch ( {
2021-09-13 12:43:07 -07:00
url ,
2021-09-13 14:29:44 -07:00
params ,
2021-09-13 12:43:07 -07:00
method ,
headers ,
2022-07-05 08:58:34 -07:00
postData : postDataBuffer ,
2021-10-01 12:11:33 -07:00
jsonData ,
2021-09-16 17:48:43 -07:00
formData ,
2021-10-01 12:11:33 -07:00
multipartData ,
2021-09-13 12:43:07 -07:00
timeout : options.timeout ,
2021-09-13 15:38:27 -07:00
failOnStatusCode : options.failOnStatusCode ,
2021-09-28 15:33:36 -07:00
ignoreHTTPSErrors : options.ignoreHTTPSErrors ,
2022-09-09 21:14:42 +02:00
maxRedirects : maxRedirects ,
2023-01-05 14:39:49 -08:00
. . . fixtures
2021-09-13 12:43:07 -07:00
} ) ;
2021-11-19 20:32:29 -08:00
return new APIResponse ( this , result . response ) ;
2021-09-13 12:43:07 -07:00
} ) ;
}
2021-09-30 14:14:29 -07:00
async storageState ( options : { path? : string } = { } ) : Promise < StorageState > {
2021-11-19 16:28:11 -08: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-09-30 14:14:29 -07:00
}
2021-09-13 12:43:07 -07:00
}
2021-11-05 16:27:49 +01:00
export class APIResponse implements api . APIResponse {
private readonly _initializer : channels.APIResponse ;
2021-09-13 12:43:07 -07:00
private readonly _headers : RawHeaders ;
2022-02-10 12:05:04 -08:00
readonly _request : APIRequestContext ;
2021-09-13 12:43:07 -07:00
2021-11-05 16:27:49 +01:00
constructor ( context : APIRequestContext , initializer : channels.APIResponse ) {
2021-09-14 18:31:35 -07:00
this . _request = context ;
2021-09-13 12:43:07 -07:00
this . _initializer = initializer ;
this . _headers = new RawHeaders ( this . _initializer . headers ) ;
}
ok ( ) : boolean {
2021-11-09 23:11:42 +01:00
return this . _initializer . status >= 200 && this . _initializer . status <= 299 ;
2021-09-13 12:43:07 -07: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-19 16:28:11 -08:00
try {
const result = await this . _request . _channel . fetchResponseBody ( { fetchUid : this._fetchUid ( ) } ) ;
if ( result . binary === undefined )
throw new Error ( 'Response has been disposed' ) ;
2022-07-05 08:58:34 -07:00
return result . binary ;
2021-11-19 16:28:11 -08:00
} catch ( e ) {
if ( e . message . includes ( kBrowserOrContextClosedError ) )
throw new Error ( 'Response has been disposed' ) ;
throw e ;
}
2021-09-13 12:43:07 -07: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-19 16:28:11 -08:00
await this . _request . _channel . disposeAPIResponse ( { fetchUid : this._fetchUid ( ) } ) ;
2021-09-13 12:43:07 -07:00
}
2021-11-16 15:42:35 -08: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 12:43:07 -07:00
_fetchUid ( ) : string {
return this . _initializer . fetchUid ;
}
2021-11-30 18:12:19 -08:00
async _fetchLog ( ) : Promise < string [ ] > {
const { log } = await this . _request . _channel . fetchLog ( { fetchUid : this._fetchUid ( ) } ) ;
return log ;
}
2021-09-13 12:43:07 -07:00
}
2021-09-16 17:48:43 -07:00
2021-11-17 18:12:26 -08:00
type ServerFilePayload = NonNullable < channels.FormField [ 'file' ] > ;
2021-09-16 17:48:43 -07:00
function filePayloadToJson ( payload : FilePayload ) : ServerFilePayload {
return {
name : payload.name ,
mimeType : payload.mimeType ,
2022-07-05 08:58:34 -07:00
buffer : payload.buffer ,
2021-09-16 17:48:43 -07:00
} ;
}
2021-09-30 14:14:29 -07:00
async function readStreamToJson ( stream : fs.ReadStream ) : Promise < ServerFilePayload > {
2021-09-16 17:48:43 -07:00
const buffer = await new Promise < Buffer > ( ( resolve , reject ) = > {
const chunks : Buffer [ ] = [ ] ;
2021-10-01 19:40:47 -07:00
stream . on ( 'data' , chunk = > chunks . push ( chunk as Buffer ) ) ;
2021-09-16 17:48:43 -07: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 ) ,
2022-07-05 08:58:34 -07:00
buffer ,
2021-09-16 17:48:43 -07:00
} ;
2021-11-11 11:12:24 -08: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 ;
2023-01-13 13:50:38 -08:00
}
function objectToArray ( map ? : { [ key : string ] : any } ) : NameValue [ ] | undefined {
if ( ! map )
return undefined ;
const result = [ ] ;
for ( const [ name , value ] of Object . entries ( map ) )
result . push ( { name , value : String ( value ) } ) ;
return result ;
}
function isFilePayload ( value : any ) : boolean {
return typeof value === 'object' && value [ 'name' ] && value [ 'mimeType' ] && value [ 'buffer' ] ;
}