text: 'use strict';
  
  // Node dependencies
  const fs = require('fs');
  const path = require('path');
  const exec = require('child_process').exec;
  
  // External dependencies
  const Hapi = require('hapi');
  const corsHeaders = require('hapi-cors-headers');
  const _ = require('lodash');
  const crypto = require('crypto');
  
  // Internal lib
  const debugLog = require('./debugLog');
  const jsonPath = require('./jsonPath');
  const createLambdaContext = require('./createLambdaContext');
  const createVelocityContext = require('./createVelocityContext');
  const createLambdaProxyContext = require('./createLambdaProxyContext');
  const renderVelocityTemplateObject = require('./renderVelocityTemplateObject');
  const createAuthScheme = require('./createAuthScheme');
  const functionHelper = require('./functionHelper');
  const Endpoint = require('./Endpoint');
  const parseResources = require('./parseResources');
  const utils = require('./utils');
  
  /*
   I'm against monolithic code like this file, but splitting it induces unneeded complexity.
   */
  class Offline {
  
    constructor(serverless, options) {
      this.serverless = serverless;
      this.service = serverless.service;
      this.serverlessLog = serverless.cli.log.bind(serverless.cli);
      this.options = options;
      this.exitCode = 0;
      this.provider = 'aws';
      this.start = this.start.bind(this);
  
      this.commands = {
        offline: {
          usage: 'Simulates API Gateway to call your lambda functions offline.',
          lifecycleEvents: ['start'],
          // add start nested options
          commands: {
            start: {
              usage: 'Simulates API Gateway to call your lambda functions offline using backward compatible initialization.',
              lifecycleEvents: [
                'init',
                'end',
              ],
            },
          },
          options: {
            prefix: {
              usage: 'Adds a prefix to every path, to send your requests to http://localhost:3000/prefix/[your_path] instead.',
              shortcut: 'p',
            },
            host: {
              usage: 'The host name to listen on. Default: localhost',
              shortcut: 'o',
            },
            port: {
              usage: 'Port to listen on. Default: 3000',
              shortcut: 'P',
            },
            stage: {
              usage: 'The stage used to populate your templates.',
              shortcut: 's',
            },
            region: {
              usage: 'The region used to populate your templates.',
              shortcut: 'r',
            },
            skipCacheInvalidation: {
              usage: 'Tells the plugin to skip require cache invalidation. A script reloading tool like Nodemon might then be needed',
              shortcut: 'c',
            },
            httpsProtocol: {
              usage: 'To enable HTTPS, specify directory (relative to your cwd, typically your project dir) for both cert.pem and key.pem files.',
              shortcut: 'H',
            },
            location: {
              usage: 'The root location of the handlers\' files.',
              shortcut: 'l',
            },
            noTimeout: {
              usage: 'Disable the timeout feature.',
              shortcut: 't',
            },
            noEnvironment: {
              usage: 'Turns off loading of your environment variables from serverless.yml. Allows the usage of tools such as PM2 or docker-compose.',
            },
            resourceRoutes: {
              usage: 'Turns on loading of your HTTP proxy settings from serverless.yml.',
            },
            dontPrintOutput: {
              usage: 'Turns off logging of your lambda outputs in the terminal.',
            },
            corsAllowOrigin: {
              usage: 'Used to build the Access-Control-Allow-Origin header for CORS support.',
            },
            corsAllowHeaders: {
              usage: 'Used to build the Access-Control-Allow-Headers header for CORS support.',
            },
            corsExposedHeaders: {
              usage: 'USed to build the Access-Control-Exposed-Headers response header for CORS support',
            },
            corsDisallowCredentials: {
              usage: 'Used to override the Access-Control-Allow-Credentials default (which is true) to false.',
            },
            apiKey: {
              usage: 'Defines the api key value to be used for endpoints marked as private. Defaults to a random hash.',
            },
            exec: {
              usage: 'When provided, a shell script is executed when the server starts up, and the server will shut down after handling this command.',
            },
            noAuth: {
              usage: 'Turns off all authorizers',
            },
            useSeparateProcesses: {
              usage: 'Uses separate node processes for handlers',
            },
            preserveTrailingSlash: {
              usage: 'Used to keep trailing slashes on the request path',
            },
          },
        },
      };
  
      this.hooks = {
        'offline:start:init': this.start.bind(this),
        'offline:start': this.start.bind(this),
        'offline:start:end': this.end.bind(this),
      };
    }
  
    printBlankLine() {
      console.log();
    }
  
    logPluginIssue() {
      this.serverlessLog('If you think this is an issue with the plugin please submit it, thanks!');
      this.serverlessLog('https://github.com/dherault/serverless-offline/issues');
    }
  
    // Entry point for the plugin (sls offline)
    start() {
      this._checkVersion();
  
      // Some users would like to know their environment outside of the handler
      process.env.IS_OFFLINE = true;
  
      return Promise.resolve(this._buildServer())
      .then(() => this._listen())
      .then(() => this.options.exec ? this._executeShellScript() : this._listenForSigInt())
      .then(() => this.end());
    }
  
    _checkVersion() {
      const version = this.serverless.version;
      if (!version.startsWith('1.')) {
        this.serverlessLog(`Offline requires Serverless v1.x.x but found ${version}. Exiting.`);
        process.exit(0);
      }
    }
  
    _listenForSigInt() {
      // Listen for ctrl+c to stop the server
      return new Promise(resolve => {
        process.on('SIGINT', () => {
          this.serverlessLog('Offline Halting...');
          resolve();
        });
      });
    }
  
    _executeShellScript() {
      const command = this.options.exec;
  
      this.serverlessLog(`Offline executing script [${command}]`);
      const options = { env: Object.assign({ IS_OFFLINE: true }, this.service.provider.environment, this.originalEnvironment) };
  
      return new Promise(resolve => {
        exec(command, options, (error, stdout, stderr) => {
          this.serverlessLog(`exec stdout: [${stdout}]`);
          this.serverlessLog(`exec stderr: [${stderr}]`);
          if (error) {
            // Use the failed command's exit code, proceed as normal so that shutdown can occur gracefully
            this.serverlessLog(`Offline error executing script [${error}]`);
            this.exitCode = error.code || 1;
          }
          resolve();
        });
      });
    }
  
    _buildServer() {
      // Maps a request id to the request's state (done: bool, timeout: timer)
      this.requests = {};
  
      // Methods
      this._setOptions();     // Will create meaningful options from cli options
      this._storeOriginalEnvironment(); // stores the original process.env for assigning upon invoking the handlers
      this._registerBabel();  // Support for ES6
      this._createServer();   // Hapijs boot
      this._createRoutes();   // API  Gateway emulation
      this._createResourceRoutes(); // HTTP Proxy defined in Resource
      this._create404Route(); // Not found handling
  
      return this.server;
    }
  
    _storeOriginalEnvironment() {
      this.originalEnvironment = _.extend({}, process.env);
    }
  
    _setOptions() {
      // Merge the different sources of values for this.options
      // Precedence is: command line options, YAML options, defaults.
  
      const defaultOpts = {
        host: 'localhost',
        location: '.',
        port: 3000,
        prefix: '/',
        stage: this.service.provider.stage,
        region: this.service.provider.region,
        noTimeout: false,
        noEnvironment: false,
        resourceRoutes: false,
        dontPrintOutput: false,
        httpsProtocol: '',
        skipCacheInvalidation: false,
        noAuth: false,
        corsAllowOrigin: '*',
        corsExposedHeaders: 'WWW-Authenticate,Server-Authorization',
        corsAllowHeaders: 'accept,content-type,x-api-key,authorization',
        corsAllowCredentials: true,
        apiKey: crypto.createHash('md5').digest('hex'),
        useSeparateProcesses: false,
        preserveTrailingSlash: false,
      };
  
      this.options = _.merge({}, defaultOpts, (this.service.custom || {})['serverless-offline'], this.options);
  
      // Prefix must start and end with '/'
      if (!this.options.prefix.startsWith('/')) this.options.prefix = `/${this.options.prefix}`;
      if (!this.options.prefix.endsWith('/')) this.options.prefix += '/';
  
      this.globalBabelOptions = ((this.service.custom || {})['serverless-offline'] || {}).babelOptions;
  
      this.velocityContextOptions = {
        stageVariables: {}, // this.service.environment.stages[this.options.stage].vars,
        stage: this.options.stage,
      };
  
      // Parse CORS options
      this.options.corsAllowOrigin = this.options.corsAllowOrigin.replace(/\s/g, '').split(',');
      this.options.corsAllowHeaders = this.options.corsAllowHeaders.replace(/\s/g, '').split(',');
      this.options.corsExposedHeaders = this.options.corsExposedHeaders.replace(/\s/g, '').split(',');
  
      if (this.options.corsDisallowCredentials) this.options.corsAllowCredentials = false;
  
      this.options.corsConfig = {
        origin: this.options.corsAllowOrigin,
        headers: this.options.corsAllowHeaders,
        credentials: this.options.corsAllowCredentials,
        exposedHeaders: this.options.corsExposedHeaders,
      };
  
      this.serverlessLog(`Starting Offline: ${this.options.stage}/${this.options.region}.`);
      debugLog('options:', this.options);
      debugLog('globalBabelOptions:', this.globalBabelOptions);
    }
  
    _registerBabel(isBabelRuntime, babelRuntimeOptions) {
      const options = isBabelRuntime ?
        babelRuntimeOptions || { presets: ['es2015'] } :
        this.globalBabelOptions;
  
      if (options) {
        debugLog('Setting babel register:', options);
  
        // We invoke babel-register only once
        if (!this.babelRegister) {
          debugLog('For the first time');
          this.babelRegister = require('babel-register')(options);
        }
      }
    }
  
    _createServer() {
      // Hapijs server creation
      this.server = new Hapi.Server({
        connections: {
          router: {
            stripTrailingSlash: !this.options.preserveTrailingSlash, // removes trailing slashes on incoming paths.
          },
        },
      });
  
      this.server.register(require('h2o2'), err => err && this.serverlessLog(err));
  
      const connectionOptions = {
        host: this.options.host,
        port: this.options.port,
      };
      const httpsDir = this.options.httpsProtocol;
  
      // HTTPS support
      if (typeof httpsDir === 'string' && httpsDir.length > 0) {
        connectionOptions.tls = {
          key: fs.readFileSync(path.resolve(httpsDir, 'key.pem'), 'ascii'),
          cert: fs.readFileSync(path.resolve(httpsDir, 'cert.pem'), 'ascii'),
        };
      }
  
      // Passes the configuration object to the server
      this.server.connection(connectionOptions);
  
      // Enable CORS preflight response
      this.server.ext('onPreResponse', corsHeaders);
    }
  
    _createRoutes() {
      const defaultContentType = 'application/json';
      const serviceRuntime = this.service.provider.runtime;
      const apiKeys = this.service.provider.apiKeys;
      const protectedRoutes = [];
  
      if (['nodejs', 'nodejs4.3', 'nodejs6.10', 'nodejs8.10', 'babel'].indexOf(serviceRuntime) === -1) {
        this.printBlankLine();
        this.serverlessLog(`Warning: found unsupported runtime '${serviceRuntime}'`);
  
        return;
      }
  
      // for simple API Key authentication model
      if (!_.isEmpty(apiKeys)) {
        this.serverlessLog(`Key with token: ${this.options.apiKey}`);
        this.serverlessLog('Remember to use x-api-key on the request headers');
      }
  
      Object.keys(this.service.functions).forEach(key => {
  
        const fun = this.service.getFunction(key);
        const funName = key;
        const servicePath = path.join(this.serverless.config.servicePath, this.options.location);
        const funOptions = functionHelper.getFunctionOptions(fun, key, servicePath);
        debugLog(`funOptions ${JSON.stringify(funOptions, null, 2)} `);
  
        this.printBlankLine();
        debugLog(funName, 'runtime', serviceRuntime, funOptions.babelOptions || '');
        this.serverlessLog(`Routes for ${funName}:`);
  
        // Adds a route for each http endpoint
        (fun.events && fun.events.length || this.serverlessLog('(none)')) && fun.events.forEach(event => {
          if (!event.http) return this.serverlessLog('(none)');
  
          // Handle Simple http setup, ex. - http: GET users/index
          if (typeof event.http === 'string') {
            const split = event.http.split(' ');
            event.http = {
              path: split[1],
              method: split[0],
            };
          }
  
          // generate an enpoint via the endpoint class
          const endpoint = new Endpoint(event.http, funOptions).generate();
  
          let firstCall = true;
  
          const integration = endpoint.integration || 'lambda-proxy';
          const epath = endpoint.path;
          const method = endpoint.method.toUpperCase();
          const requestTemplates = endpoint.requestTemplates;
  
          // Prefix must start and end with '/' BUT path must not end with '/'
          let fullPath = this.options.prefix + (epath.startsWith('/') ? epath.slice(1) : epath);
          if (fullPath !== '/' && fullPath.endsWith('/')) fullPath = fullPath.slice(0, -1);
          fullPath = fullPath.replace(/\+}/g, '*}');
  
          if (_.eq(event.http.private, true)) {
            protectedRoutes.push(`${method}#${fullPath}`);
          }
  
          this.serverlessLog(`${method} ${fullPath}`);
  
          // If the endpoint has an authorization function, create an authStrategy for the route
          const authStrategyName = this.options.noAuth ? null : this._configureAuthorization(endpoint, funName, method, epath, servicePath);
  
          let cors = null;
          if (endpoint.cors) {
            cors = {
              origin: endpoint.cors.origins || this.options.corsConfig.origin,
              headers: endpoint.cors.headers || this.options.corsConfig.headers,
              credentials: endpoint.cors.credentials || this.options.corsConfig.credentials,
              exposedHeaders: this.options.corsConfig.exposedHeaders,
            };
          }
  
          // Route creation
          const routeMethod = method === 'ANY' ? '*' : method;
          const routeConfig = {
            cors,
            auth: authStrategyName,
            timeout: { socket: false },
          };
  
          // skip HEAD routes as hapi will fail with 'Method name not allowed: HEAD ...'
          // for more details, check https://github.com/dherault/serverless-offline/issues/204
          if (routeMethod === 'HEAD') {
            this.serverlessLog('HEAD method event detected. Skipping HAPI server route mapping ...');
  
            return;
          }
  
          if (routeMethod !== 'HEAD' && routeMethod !== 'GET') {
            // maxBytes: Increase request size from 1MB default limit to 10MB.
            // Cf AWS API GW payload limits.
            routeConfig.payload = { parse: false, maxBytes: 1024 * 1024 * 10 };
          }
  
          this.server.route({
            method: routeMethod,
            path: fullPath,
            config: routeConfig,
            handler: (request, reply) => { // Here we go
              // Payload processing
              const encoding = utils.detectEncoding(request);
  
              request.payload = request.payload && request.payload.toString(encoding);
              request.rawPayload = request.payload;
  
              // Headers processing
              // Hapi lowercases the headers whereas AWS does not
              // so we recreate a custom headers object from the raw request
              const headersArray = request.raw.req.rawHeaders;
  
              // During tests, `server.inject` uses *shot*, a package
              // for performing injections that does not entirely mimick
              // Hapi's usual request object. rawHeaders are then missing
              // Hence the fallback for testing
  
              // Normal usage
              if (headersArray) {
                const unprocessedHeaders = {};
  
                for (let i = 0; i < headersArray.length; i += 2) {
                  unprocessedHeaders[headersArray[i]] = headersArray[i + 1];
                }
  
                request.unprocessedHeaders = unprocessedHeaders;
              }
              // Lib testing
              else {
                request.unprocessedHeaders = request.headers;
                // console.log('request.unprocessedHeaders:', request.unprocessedHeaders);
              }
  
  
              // Incomming request message
              this.printBlankLine();
              this.serverlessLog(`${method} ${request.path} (λ: ${funName})`);
              if (firstCall) {
                this.serverlessLog('The first request might take a few extra seconds');
                firstCall = false;
              }
  
              // this.serverlessLog(protectedRoutes);
              // Check for APIKey
              if (_.includes(protectedRoutes, `${routeMethod}#${fullPath}`) || _.includes(protectedRoutes, `ANY#${fullPath}`)) {
                const errorResponse = response => response({ message: 'Forbidden' }).code(403).type('application/json').header('x-amzn-ErrorType', 'ForbiddenException');
                if ('x-api-key' in request.headers) {
                  const requestToken = request.headers['x-api-key'];
                  if (requestToken !== this.options.apiKey) {
                    debugLog(`Method ${method} of function ${funName} token ${requestToken} not valid`);
  
                    return errorResponse(reply);
                  }
                }
                else {
                  debugLog(`Missing x-api-key on private function ${funName}`);
  
                  return errorResponse(reply);
                }
              }
              // Shared mutable state is the root of all evil they say
              const requestId = utils.randomId();
              this.requests[requestId] = { done: false };
              this.currentRequestId = requestId;
  
              // Holds the response to do async op
              const response = reply.response().hold();
              const contentType = request.mime || defaultContentType;
  
              // default request template to '' if we don't have a definition pushed in from serverless or endpoint
              const requestTemplate = typeof requestTemplates !== 'undefined' && integration === 'lambda' ? requestTemplates[contentType] : '';
  
              // https://hapijs.com/api#route-configuration doesn't seem to support selectively parsing
              // so we have to do it ourselves
              const contentTypesThatRequirePayloadParsing = ['application/json', 'application/vnd.api+json'];
              if (contentTypesThatRequirePayloadParsing.indexOf(contentType) !== -1) {
                try {
                  request.payload = JSON.parse(request.payload);
                }
                catch (err) {
                  debugLog('error in converting request.payload to JSON:', err);
                }
              }
  
              debugLog('requestId:', requestId);
              debugLog('contentType:', contentType);
              debugLog('requestTemplate:', requestTemplate);
              debugLog('payload:', request.payload);
  
              /* HANDLER LAZY LOADING */
  
              let handler; // The lambda function
  
              try {
                if (this.options.noEnvironment) {
                  // This evict errors in server when we use aws services like ssm
                  const baseEnvironment = {
                    AWS_ACCESS_KEY_ID: 'dev',
                    AWS_SECRET_ACCESS_KEY: 'dev',
                    AWS_REGION: 'dev',
                  };
  
                  process.env = _.extend({}, baseEnvironment);
                }
                else {
                  Object.assign(process.env, this.service.provider.environment, this.service.functions[key].environment);
                }
                Object.assign(process.env, this.originalEnvironment);
                process.env._HANDLER = fun.handler;
                handler = functionHelper.createHandler(funOptions, this.options);
              }
              catch (err) {
                return this._reply500(response, `Error while loading ${funName}`, err, requestId);
              }
  
              /* REQUEST TEMPLATE PROCESSING (event population) */
  
              let event = {};
  
              if (integration === 'lambda') {
                if (requestTemplate) {
                  try {
                    debugLog('_____ REQUEST TEMPLATE PROCESSING _____');
                    // Velocity templating language parsing
                    const velocityContext = createVelocityContext(request, this.velocityContextOptions, request.payload || {});
                    event = renderVelocityTemplateObject(requestTemplate, velocityContext);
                  }
                  catch (err) {
                    return this._reply500(response, `Error while parsing template "${contentType}" for ${funName}`, err, requestId);
                  }
                }
                else if (typeof request.payload === 'object') {
                  event = request.payload || {};
                }
              }
              else if (integration === 'lambda-proxy') {
                event = createLambdaProxyContext(request, this.options, this.velocityContextOptions.stageVariables);
              }
  
              event.isOffline = true;
  
              if (this.serverless.service.custom && this.serverless.service.custom.stageVariables) {
                event.stageVariables = this.serverless.service.custom.stageVariables;
              }
              else if (integration !== 'lambda-proxy') {
                event.stageVariables = {};
              }
  
              debugLog('event:', event);
  
              // We create the context, its callback (context.done/succeed/fail) will send the HTTP response
              const lambdaContext = createLambdaContext(fun, (err, data, fromPromise) => {
                // Everything in this block happens once the lambda function has resolved
                debugLog('_____ HANDLER RESOLVED _____');
  
                // Timeout clearing if needed
                if (this._clearTimeout(requestId)) return;
  
                // User should not call context.done twice
                if (this.requests[requestId].done) {
                  this.printBlankLine();
                  const warning = fromPromise
                    ? `Warning: handler '${funName}' returned a promise and also use a callback!\nThis is problematic and might cause issues in you lambda.`
                    : `Warning: context.done called twice within handler '${funName}'!`;
                  this.serverlessLog(warning);
                  debugLog('requestId:', requestId);
  
                  return;
                }
  
                this.requests[requestId].done = true;
  
                let result = data;
                let responseName = 'default';
                const responseContentType = endpoint.responseContentType;
                const contentHandling = endpoint.contentHandling;
  
                /* RESPONSE SELECTION (among endpoint's possible responses) */
  
                // Failure handling
                let errorStatusCode = 0;
                if (err) {
  
                  const errorMessage = (err.message || err).toString();
  
                  const re = /\[(\d{3})]/;
                  const found = errorMessage.match(re);
                  if (found && found.length > 1) {
                    errorStatusCode = found[1];
                  }
                  else {
                    errorStatusCode = '500';
                  }
  
                  // Mocks Lambda errors
                  result = {
                    errorMessage,
                    errorType: err.constructor.name,
                    stackTrace: this._getArrayStackTrace(err.stack),
                  };
  
                  this.serverlessLog(`Failure: ${errorMessage}`);
                  if (result.stackTrace) {
                    debugLog(result.stackTrace.join('\n  '));
                  }
  
                  for (const key in endpoint.responses) {
                    if (key !== 'default' && errorMessage.match(`^${endpoint.responses[key].selectionPattern || key}$`)) {
                      responseName = key;
                      break;
                    }
                  }
                }
  
                debugLog(`Using response '${responseName}'`);
                const chosenResponse = endpoint.responses[responseName];
  
                /* RESPONSE PARAMETERS PROCCESSING */
  
                const responseParameters = chosenResponse.responseParameters;
  
                if (_.isPlainObject(responseParameters)) {
  
                  const responseParametersKeys = Object.keys(responseParameters);
  
                  debugLog('_____ RESPONSE PARAMETERS PROCCESSING _____');
                  debugLog(`Found ${responseParametersKeys.length} responseParameters for '${responseName}' response`);
  
                  responseParametersKeys.forEach(key => {
  
                    // responseParameters use the following shape: "key": "value"
                    const value = responseParameters[key];
                    const keyArray = key.split('.'); // eg: "method.response.header.location"
                    const valueArray = value.split('.'); // eg: "integration.response.body.redirect.url"
  
                    debugLog(`Processing responseParameter "${key}": "${value}"`);
  
                    // For now the plugin only supports modifying headers
                    if (key.startsWith('method.response.header') && keyArray[3]) {
  
                      const headerName = keyArray.slice(3).join('.');
                      let headerValue;
                      debugLog('Found header in left-hand:', headerName);
  
                      if (value.startsWith('integration.response')) {
                        if (valueArray[2] === 'body') {
  
                          debugLog('Found body in right-hand');
                          headerValue = (valueArray[3] ? jsonPath(result, valueArray.slice(3).join('.')) : result).toString();
  
                        }
                        else {
                          this.printBlankLine();
                          this.serverlessLog(`Warning: while processing responseParameter "${key}": "${value}"`);
                          this.serverlessLog(`Offline plugin only supports "integration.response.body[.JSON_path]" right-hand responseParameter. Found "${value}" instead. Skipping.`);
                          this.logPluginIssue();
                          this.printBlankLine();
                        }
                      }
                      else {
                        headerValue = value.match(/^'.*'$/) ? value.slice(1, -1) : value; // See #34
                      }
                      // Applies the header;
                      debugLog(`Will assign "${headerValue}" to header "${headerName}"`);
                      response.header(headerName, headerValue);
  
                    }
                    else {
                      this.printBlankLine();
                      this.serverlessLog(`Warning: while processing responseParameter "${key}": "${value}"`);
                      this.serverlessLog(`Offline plugin only supports "method.response.header.PARAM_NAME" left-hand responseParameter. Found "${key}" instead. Skipping.`);
                      this.logPluginIssue();
                      this.printBlankLine();
                    }
                  });
                }
  
                let statusCode = 200;
  
                if (integration === 'lambda') {
                  /* RESPONSE TEMPLATE PROCCESSING */
                  // If there is a responseTemplate, we apply it to the result
                  const responseTemplates = chosenResponse.responseTemplates;
  
                  if (_.isPlainObject(responseTemplates)) {
  
                    const responseTemplatesKeys = Object.keys(responseTemplates);
  
                    if (responseTemplatesKeys.length) {
  
                      // BAD IMPLEMENTATION: first key in responseTemplates
                      const responseTemplate = responseTemplates[responseContentType];
  
                      if (responseTemplate && responseTemplate !== '\n') {
  
                        debugLog('_____ RESPONSE TEMPLATE PROCCESSING _____');
                        debugLog(`Using responseTemplate '${responseContentType}'`);
  
                        try {
                          const reponseContext = createVelocityContext(request, this.velocityContextOptions, result);
                          result = renderVelocityTemplateObject({ root: responseTemplate }, reponseContext).root;
                        }
                        catch (error) {
                          this.serverlessLog(`Error while parsing responseTemplate '${responseContentType}' for lambda ${funName}:`);
                          console.log(error.stack);
                        }
                      }
                    }
                  }
  
                  /* HAPIJS RESPONSE CONFIGURATION */
  
                  statusCode = errorStatusCode !== 0 ? errorStatusCode : (chosenResponse.statusCode || 200);
  
                  if (!chosenResponse.statusCode) {
                    this.printBlankLine();
                    this.serverlessLog(`Warning: No statusCode found for response "${responseName}".`);
                  }
  
                  response.header('Content-Type', responseContentType, {
                    override: false, // Maybe a responseParameter set it already. See #34
                  });
                  response.statusCode = statusCode;
                  if (contentHandling === 'CONVERT_TO_BINARY') {
                    response.encoding = 'binary';
                    response.source = Buffer.from(result, 'base64');
                    response.variety = 'buffer';
                  }
                  else {
                    if (result.body && typeof result.body !== 'string') {
                      return this._reply500(response, `According to the API Gateway specs, the body content must be stringified. Check your Lambda response and make sure you are invoking JSON.stringify(YOUR_CONTENT) on your body object`, {}, requestId);
                    }
                    response.source = result;
                  }
                }
                else if (integration === 'lambda-proxy') {
                  response.statusCode = statusCode = result.statusCode || 200;
  
                  const defaultHeaders = { 'Content-Type': 'application/json' };
  
                  Object.assign(defaultHeaders, result.headers);
  
                  Object.keys(defaultHeaders).forEach(header => {
                    response.header(header, defaultHeaders[header]);
                  });
  
                  if (!_.isUndefined(result.body)) {
                    if (result.isBase64Encoded) {
                      response.encoding = 'binary';
                      response.source = Buffer.from(result.body, 'base64');
                      response.variety = 'buffer';
                    }
                    else {
                        if (result.body && typeof result.body !== 'string') {
                          return this._reply500(response, `According to the API Gateway specs, the body content must be stringified. Check your Lambda response and make sure you are invoking JSON.stringify(YOUR_CONTENT) on your body object`, {}, requestId);
                        }
                      response.source = result.body;
                    }
                  }
                }
  
                // Log response
                let whatToLog = result;
  
                try {
                  whatToLog = JSON.stringify(result);
                }
                catch (error) {
                  // nothing
                }
                finally {
                  if (!this.options.dontPrintOutput) this.serverlessLog(err ? `Replying ${statusCode}` : `[${statusCode}] ${whatToLog}`);
                  debugLog('requestId:', requestId);
                }
  
                // Bon voyage!
                response.send();
              });
  
              // Now we are outside of createLambdaContext, so this happens before the handler gets called:
  
              // We cannot use Hapijs's timeout feature because the logic above can take a significant time, so we implement it ourselves
              this.requests[requestId].timeout = this.options.noTimeout ? null : setTimeout(
                this._replyTimeout.bind(this, response, funName, funOptions.funTimeout, requestId),
                funOptions.funTimeout
              );
  
              // Finally we call the handler
              debugLog('_____ CALLING HANDLER _____');
              try {
                const x = handler(event, lambdaContext, lambdaContext.done);
  
                // Promise support
                if ((serviceRuntime === 'nodejs8.10' || serviceRuntime === 'babel') && !this.requests[requestId].done) {
                  if (x && typeof x.then === 'function' && typeof x.catch === 'function') x.then(lambdaContext.succeed).catch(lambdaContext.fail);
                  else if (x instanceof Error) lambdaContext.fail(x);
                }
              }
              catch (error) {
                return this._reply500(response, `Uncaught error in your '${funName}' handler`, error, requestId);
              }
            },
          });
        });
      });
    }
  
    _configureAuthorization(endpoint, funName, method, epath, servicePath) {
      let authStrategyName = null;
      if (endpoint.authorizer) {
        let authFunctionName = endpoint.authorizer;
        if (typeof authFunctionName === 'string' && authFunctionName.toUpperCase() === 'AWS_IAM') {
          this.serverlessLog('WARNING: Serverless Offline does not support the AWS_IAM authorization type');
  
          return null;
        }
        if (typeof endpoint.authorizer === 'object') {
          if (endpoint.authorizer.type && endpoint.authorizer.type.toUpperCase() === 'AWS_IAM') {
            this.serverlessLog('WARNING: Serverless Offline does not support the AWS_IAM authorization type');
  
            return null;
          }
          if (endpoint.authorizer.arn) {
            this.serverlessLog(`WARNING: Serverless Offline does not support non local authorizers: ${endpoint.authorizer.arn}`);
  
            return authStrategyName;
          }
          authFunctionName = endpoint.authorizer.name;
        }
  
        this.serverlessLog(`Configuring Authorization: ${endpoint.path} ${authFunctionName}`);
  
        const authFunction = this.service.getFunction(authFunctionName);
  
        if (!authFunction) return this.serverlessLog(`WARNING: Authorization function ${authFunctionName} does not exist`);
  
        const authorizerOptions = {
          resultTtlInSeconds: '300',
          identitySource: 'method.request.header.Authorization',
        };
  
        if (typeof endpoint.authorizer === 'string') {
          authorizerOptions.name = authFunctionName;
        }
        else {
          Object.assign(authorizerOptions, endpoint.authorizer);
        }
  
        // Create a unique scheme per endpoint
        // This allows the methodArn on the event property to be set appropriately
        const authKey = `${funName}-${authFunctionName}-${method}-${epath}`;
        const authSchemeName = `scheme-${authKey}`;
        authStrategyName = `strategy-${authKey}`; // set strategy name for the route config
  
        debugLog(`Creating Authorization scheme for ${authKey}`);
  
        // Create the Auth Scheme for the endpoint
        const scheme = createAuthScheme(
          authFunction,
          authorizerOptions,
          funName,
          epath,
          this.options,
          this.serverlessLog,
          servicePath,
          this.serverless
        );
  
        // Set the auth scheme and strategy on the server
        this.server.auth.scheme(authSchemeName, scheme);
        this.server.auth.strategy(authStrategyName, authSchemeName);
      }
  
      return authStrategyName;
    }
  
    // All done, we can listen to incomming requests
    _listen() {
      return new Promise((resolve, reject) => {
        this.server.start(err => {
          if (err) return reject(err);
  
          this.printBlankLine();
          this.serverlessLog(`Offline listening on http${this.options.httpsProtocol ? 's' : ''}://${this.options.host}:${this.options.port}`);
  
          resolve(this.server);
        });
      });
    }
  
    end() {
      this.serverlessLog('Halting offline server');
      this.server.stop({ timeout: 5000 })
      .then(() => process.exit(this.exitCode));
    }
  
    // Bad news
    _reply500(response, message, err, requestId) {
  
      if (this._clearTimeout(requestId)) return;
  
      this.requests[requestId].done = true;
  
      const stackTrace = this._getArrayStackTrace(err.stack);
  
      this.serverlessLog(message);
      if (stackTrace && stackTrace.length > 0) {
        console.log(stackTrace);
      }
      else {
        console.log(err);
      }
  
      /* eslint-disable no-param-reassign */
      response.statusCode = 200; // APIG replies 200 by default on failures
      response.source = {
        errorMessage: message,
        errorType: err.constructor.name,
        stackTrace,
        offlineInfo: 'If you believe this is an issue with the plugin please submit it, thanks. https://github.com/dherault/serverless-offline/issues',
      };
      /* eslint-enable no-param-reassign */
      this.serverlessLog('Replying error in handler');
      response.send();
    }
  
    _replyTimeout(response, funName, funTimeout, requestId) {
      if (this.currentRequestId !== requestId) return;
  
      this.requests[requestId].done = true;
  
      this.serverlessLog(`Replying timeout after ${funTimeout}ms`);
      /* eslint-disable no-param-reassign */
      response.statusCode = 503;
      response.source = `[Serverless-Offline] Your λ handler '${funName}' timed out after ${funTimeout}ms.`;
      /* eslint-enable no-param-reassign */
      response.send();
    }
  
    _clearTimeout(requestId) {
      const timeout = this.requests[requestId].timeout;
      if (timeout && timeout._called) return true;
      clearTimeout(timeout);
    }
  
    _createResourceRoutes() {
      if (!this.options.resourceRoutes) return true;
      const resourceRoutesOptions = this.options.resourceRoutes;
      const resourceRoutes = parseResources(this.service.resources);
  
      if (_.isEmpty(resourceRoutes)) return true;
  
      this.printBlankLine();
      this.serverlessLog('Routes defined in resources:');
  
      Object.keys(resourceRoutes).forEach(methodId => {
        const resourceRoutesObj = resourceRoutes[methodId];
        const path = resourceRoutesObj.path;
        const method = resourceRoutesObj.method;
        const isProxy = resourceRoutesObj.isProxy;
        const proxyUri = resourceRoutesObj.proxyUri;
        const pathResource = resourceRoutesObj.pathResource;
  
        if (!isProxy) {
          return this.serverlessLog(`WARNING: Only HTTP_PROXY is supported. Path '${pathResource}' is ignored.`);
        }
        if (!path) {
          return this.serverlessLog(`WARNING: Could not resolve path for '${methodId}'.`);
        }
  
        let fullPath = this.options.prefix + (pathResource.startsWith('/') ? pathResource.slice(1) : pathResource);
        if (fullPath !== '/' && fullPath.endsWith('/')) fullPath = fullPath.slice(0, -1);
        fullPath = fullPath.replace(/\+}/g, '*}');
  
        const proxyUriOverwrite = resourceRoutesOptions[methodId] || {};
        const proxyUriInUse = proxyUriOverwrite.Uri || proxyUri;
  
        if (!proxyUriInUse) {
          return this.serverlessLog(`WARNING: Could not load Proxy Uri for '${methodId}'`);
        }
  
        const routeMethod = method === 'ANY' ? '*' : method;
        const routeConfig = { cors: this.options.corsConfig };
  
        if (routeMethod !== 'HEAD' && routeMethod !== 'GET') {
          routeConfig.payload = { parse: false };
        }
  
        this.serverlessLog(`${method} ${fullPath} -> ${proxyUriInUse}`);
        this.server.route({
          method: routeMethod,
          path: fullPath,
          config: routeConfig,
          handler: (request, reply) => {
            const params = request.params;
            let resultUri = proxyUriInUse;
  
            Object.keys(params).forEach(key => {
              resultUri = resultUri.replace(`{${key}}`, params[key]);
            });
            this.serverlessLog(`PROXY ${request.method} ${request.url.path} -> ${resultUri}`);
            reply.proxy({ uri: resultUri, passThrough: true });
          },
        });
      });
    }
  
    _create404Route() {
      // If a {proxy+} route exists, don't conflict with it
      if (this.server.match('*', '/{p*}')) return;
  
      this.server.route({
        method: '*',
        path: '/{p*}',
        config: { cors: this.options.corsConfig },
        handler: (request, reply) => {
          const response = reply({
            statusCode: 404,
            error: 'Serverless-offline: route not found.',
            currentRoute: `${request.method} - ${request.path}`,
            existingRoutes: this.server.table()[0].table
              .filter(route => route.path !== '/{p*}') // Exclude this (404) route
              .sort((a, b) => a.path <= b.path ? -1 : 1) // Sort by path
              .map(route => `${route.method} - ${route.path}`), // Human-friendly result
          });
          response.statusCode = 404;
        },
      });
    }
  
    _getArrayStackTrace(stack) {
      if (!stack) return null;
  
      const splittedStack = stack.split('\n');
  
      return splittedStack.slice(0, splittedStack.findIndex(item => item.match(/server.route.handler.createLambdaContext/))).map(line => line.trim());
    }
  
    _logAndExit() {
      console.log.apply(null, arguments);
      process.exit(0);
    }
  }
  
  // Serverless exits with code 1 when a promise rejection is unhandled. Not AWS.
  // Users can still use their own unhandledRejection event though.
  process.removeAllListeners('unhandledRejection');
  
  module.exports = Offline;
  