All files / webdriver/src request.js

100% Statements 49/49
96% Branches 48/50
100% Functions 6/6
100% Lines 48/48

Press n or j to go to the next uncovered block, b, p or k for the previous block.

1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160                          4x 4x             36x 36x 36x 36x 36x 36x 36x                         12x 12x 12x       29x                 29x 11x 11x                   29x 1x     28x                     28x 1x                 28x   28x       21x   21x 9x     21x 21x           21x         2x 1x     1x           20x 12x 12x             8x 1x 1x 1x             7x 2x 2x 2x     5x 5x 5x 5x 5x        
import url from 'url'
import http from 'http'
import path from 'path'
import https from 'https'
import merge from 'lodash.merge'
import request from 'request'
import EventEmitter from 'events'
 
import logger from '@wdio/logger'
 
import { isSuccessfulResponse, getErrorFromResponseBody } from './utils'
import pkg from '../package.json'
 
const log = logger('webdriver')
const agents = {
    http: new http.Agent({ keepAlive: true }),
    https: new https.Agent({ keepAlive: true })
}
 
export default class WebDriverRequest extends EventEmitter {
    constructor (method, endpoint, body, isHubCommand) {
        super()
        this.body = body
        this.method = method
        this.endpoint = endpoint
        this.isHubCommand = isHubCommand
        this.requiresSessionId = this.endpoint.match(/:sessionId/)
        this.defaultOptions = {
            method,
            followAllRedirects: true,
            json: true,
            headers: {
                'Connection': 'keep-alive',
                'Accept': 'application/json',
                'User-Agent': 'webdriver/' + pkg.version
            }
        }
    }
 
    makeRequest (options, sessionId) {
        const fullRequestOptions = merge({}, this.defaultOptions, this._createOptions(options, sessionId))
        this.emit('request', fullRequestOptions)
        return this._request(fullRequestOptions, options.connectionRetryCount)
    }
 
    _createOptions (options, sessionId) {
        const requestOptions = {
            agent: options.agent || agents[options.protocol],
            headers: typeof options.headers === 'object' ? options.headers : {},
            qs: typeof options.queryParams === 'object' ? options.queryParams : {}
        }
 
        /**
         * only apply body property if existing
         */
        if (this.body && (Object.keys(this.body).length || this.method === 'POST')) {
            requestOptions.body = this.body
            requestOptions.headers = merge({}, requestOptions.headers, {
                'Content-Length': Buffer.byteLength(JSON.stringify(requestOptions.body), 'UTF-8')
            })
        }
 
        /**
         * if we don't have a session id we set it here, unless we call commands that don't require session ids, for
         * example /sessions. The call to /sessions is not connected to a session itself and it therefore doesn't
         * require it
         */
        if (this.requiresSessionId && !sessionId) {
            throw new Error('A sessionId is required for this command')
        }
 
        requestOptions.uri = url.parse(
            `${options.protocol}://` +
            `${options.hostname}:${options.port}` +
            (this.isHubCommand
                ? this.endpoint
                : path.join(options.path, this.endpoint.replace(':sessionId', sessionId)))
        )
 
        /**
         * send authentication credentials only when creating new session
         */
        if (this.endpoint === '/session' && options.user && options.key) {
            requestOptions.auth = {
                user: options.user,
                pass: options.key
            }
        }
 
        /**
         * if the environment variable "STRICT_SSL" is defined as "false", it doesn't require SSL certificates to be valid.
         */
        requestOptions.strictSSL = !(process.env.STRICT_SSL === 'false' || process.env.strict_ssl === 'false')
 
        return requestOptions
    }
 
    _request (fullRequestOptions, totalRetryCount = 0, retryCount = 0) {
        log.info(`[${fullRequestOptions.method}] ${fullRequestOptions.uri.href}`)
 
        if (fullRequestOptions.body && Object.keys(fullRequestOptions.body).length) {
            log.info('DATA', fullRequestOptions.body)
        }
 
        return new Promise((resolve, reject) => request(fullRequestOptions, (err, response, body) => {
            const error = err || getErrorFromResponseBody(body)
 
            /**
             * hub commands don't follow standard response formats
             * and can have empty bodies
             */
            if (this.isHubCommand) {
                /**
                 * if body contains HTML the command was called on a node
                 * directly without using a hub, therefor throw
                 */
                if (typeof body === 'string' && body.startsWith('<!DOCTYPE html>')) {
                    return reject(new Error('Command can only be called to a Selenium Hub'))
                }
 
                body = { value: body || null }
            }
 
            /**
             * Resolve only if successful response
             */
            if (!err && isSuccessfulResponse(response.statusCode, body)) {
                this.emit('response', { result: body })
                return resolve(body)
            }
 
            /**
             *  stop retrying as this will never be successful.
             *  we will handle this at the elementErrorHandler
             */
            if(error.name === 'stale element reference') {
                log.warn('Request encountered a stale element - terminating request')
                this.emit('response', { error })
                return reject(error)
            }
 
            /**
             * stop retrying if totalRetryCount was exceeded or there is no reason to
             * retry, e.g. if sessionId is invalid
             */
            if (retryCount >= totalRetryCount || error.message.includes('invalid session id')) {
                log.error('Request failed due to', error)
                this.emit('response', { error })
                return reject(error)
            }
 
            ++retryCount
            this.emit('retry', { error, retryCount })
            log.warn('Request failed due to', error.message)
            log.info(`Retrying ${retryCount}/${totalRetryCount}`)
            this._request(fullRequestOptions, totalRetryCount, retryCount).then(resolve, reject)
        }))
    }
}