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