Repository: Miserlou/Zappa

Issue:
Flask `request.url` is wrong when making requests to the `amazonaws.com` endpoint
## Context
When Zappa is running for a project which has a `settings.DOMAIN` set the Flask request is always created without a script name. This is the correct behaviour for requests which were received on the domain (e.g. `GET https://mydomain.com/api`) but is the incorrect behaviour for requests which were received on the Amazon AWS url (e.g. `https://blahblahblah.execute-api.us-east-1.amazonaws.com/dev/api`).

This is a bug because it means that anyone using `request.url` inside their Flask code will see the URL without the stage for the requests received on the AWS url (e.g. `request.url == 'https://blahblahblah.execute-api.us-east-1.amazonaws.com/api'`).

## Possible Fix
Decide the `script_name` dynamically based on the request, rather than just based on the Zappa settings.

## Steps to Reproduce
```
$ http https://yrclxq7qw0.execute-api.eu-west-1.amazonaws.com/dev/info
HTTP/1.1 200 OK
Connection: keep-alive
Content-Length: 79
Content-Type: text/html; charset=utf-8
Date: Mon, 24 Jul 2017 11:39:35 GMT
Via: 1.1 a87358124376a016816b92e640349672.cloudfront.net (CloudFront)
X-Amz-Cf-Id: AGDtqDA8WcZh0KjQr-CVSSEdFiKqBV0in555tJEOIf8BHe8uoPf4fA==
X-Amzn-Trace-Id: sampled=0;root=1-5975dc77-0036a6f483e8dcb741fd275c
X-Cache: Miss from cloudfront
x-amzn-Remapped-Content-Length: 79
x-amzn-RequestId: c4c14366-7064-11e7-ae38-cd0b527c41b7

{'request.url': u'https://yrclxq7qw0.execute-api.eu-west-1.amazonaws.com/info'}
```

Here's the code for the relevant view:

```
from flask import Flask
from flask import request


the_app = Flask(__name__)


@the_app.route('/info')
def return_info():
    return str({'request.url': request.url})
```

Full lambda code is available at

## Your Environment
* Zappa version used: 0.43.1
* Operating System and Python version: Python 2.7
* The output of `pip freeze`:
```
appdirs==1.4.3
appnope==0.1.0
argcomplete==1.8.2
asn1crypto==0.22.0
awacs==0.6.2
awscli==1.11.123
backports.functools-lru-cache==1.4
backports.shutil-get-terminal-size==1.0.0
backports.tempfile==1.0rc1
backports.weakref==1.0rc1
base58==0.2.4
bbm-wrench==2.42.1
bcrypt==3.1.3
boto3==1.4.4
botocore==1.5.86
bravado==9.0.7
bravado-core==4.8.0
bson==0.4.8
cachetools==2.0.0
certifi==2017.4.17
cffi==1.10.0
chardet==3.0.4
click==6.7
clickclick==1.2.2
colorama==0.3.7
connexion==1.1.12
cryptography==2.0
decorator==4.1.2
docutils==0.13.1
durationpy==0.4
enum34==1.1.6
Fabric3==1.13.1.post1
Flask==0.12.2
Flask-HAL==1.0.4
formic==0.9b8
funcsigs==1.0.2
functools32==3.2.3.post2
future==0.16.0
futures==3.1.1
hjson==2.0.7
idna==2.5
inflection==0.3.1
ipaddress==1.0.18
ipdb==0.10.3
ipython==5.4.1
ipython-genutils==0.2.0
isodate==0.5.4
itsdangerous==0.24
Jinja2==2.9.6
jmespath==0.9.3
jsonpatch==1.16
jsonpointer==1.10
jsonschema==2.6.0
kappa==0.6.0
lambda-packages==0.16.1
MarkupSafe==1.0
mock==2.0.0
mongoengine==0.13.0
packaging==16.8
paramiko==2.2.1
pathlib==1.0.1
pathlib2==2.3.0
pbr==3.1.1
pexpect==4.2.1
pickleshare==0.7.4
pkginfo==1.4.1
placebo==0.8.1
pprintpp==0.3.0
prompt-toolkit==1.0.14
ptyprocess==0.5.2
pyasn1==0.2.3
pycparser==2.18
Pygments==2.2.0
pymongo==3.4.0
PyNaCl==1.1.2
pyOpenSSL==17.2.0
pyparsing==2.2.0
python-dateutil==2.6.1
python-dotenv==0.6.4
python-slugify==1.2.4
python-string-utils==0.6.0
pytz==2017.2
PyYAML==3.12
requests==2.18.1
rsa==3.4.2
s3pypi==0.4.0
s3transfer==0.1.10
scandir==1.5
simplegeneric==0.8.1
simplejson==3.11.1
six==1.10.0
stacker==1.0.4
swagger-spec-validator==2.1.0
tabulate==0.7.7
toml==0.9.2
tqdm==4.14.0
traitlets==4.3.2
troposphere==1.9.4
typing==3.6.1
Unidecode==0.4.21
urllib3==1.21.1
wcwidth==0.1.7
Werkzeug==0.12
wsgi-request-logger==0.4.6
WTForms==2.1
zappa==0.43.1
```
* Link to your project (optional): https://github.com/jakul/zappa-request-path-problem
* Your `zappa_settings.py`:
```
dev:
  app_function: app.wsgi.the_app
  aws_region: eu-west-1
  domain: api.example.com
  keep_warm: false
  manage_roles: false
  project_name: zappa-request-path-problem
  role_name: zappa-execution-role-dev
  s3_bucket: zappa-uploads-dev
```



Discussion:

Resolving patch:
diff --git a/zappa/handler.py b/zappa/handler.py
--- a/zappa/handler.py
+++ b/zappa/handler.py
@@ -406,15 +406,30 @@ def handler(self, event, context):
             # This is a normal HTTP request
             if event.get('httpMethod', None):
 
-                if settings.DOMAIN:
-                    # If we're on a domain, we operate normally
-                    script_name = ''
+                script_name = ''
+                headers = event.get('headers')
+                if headers:
+                    host = headers.get('Host')
                 else:
-                    # But if we're not, then our base URL
-                    # will be something like
-                    # https://blahblahblah.execute-api.us-east-1.amazonaws.com/dev
-                    # So, we need to make sure the WSGI app knows this.
-                    script_name = '/' + settings.API_STAGE
+                    host = None
+
+                if host:
+                    if 'amazonaws.com' in host:
+                        # The path provided in th event doesn't include the
+                        # stage, so we must tell Flask to include the API
+                        # stage in the url it calculates. See https://github.com/Miserlou/Zappa/issues/1014
+                        script_name = '/' + settings.API_STAGE
+                else:
+                    # This is a test request sent from the AWS console
+                    if settings.DOMAIN:
+                        # Assume the requests received will be on the specified
+                        # domain. No special handling is required
+                        pass
+                    else:
+                        # Assume the requests received will be to the
+                        # amazonaws.com endpoint, so tell Flask to include the
+                        # API stage
+                        script_name = '/' + settings.API_STAGE
 
                 # Create the environment for WSGI and handle the request
                 environ = create_wsgi_request(


Test patch:
diff --git a/tests/test_handler.py b/tests/test_handler.py
--- a/tests/test_handler.py
+++ b/tests/test_handler.py
@@ -38,4 +38,92 @@ def test_run_function(self):
             LambdaHandler.run_function(unsupported, 'e', 'c')
             self.fail('Exception expected')
         except RuntimeError as e:
-            pass
\ No newline at end of file
+            pass
+
+    def test_wsgi_script_name_on_aws_url(self):
+        """
+        Ensure that requests to the amazonaws.com host for an API with a
+        domain have the correct request.url
+        """
+        lh = LambdaHandler('tests.test_wsgi_script_name_settings')
+
+        event = {
+            'body': '',
+            'resource': '/{proxy+}',
+            'requestContext': {},
+            'queryStringParameters': {},
+            'headers': {
+                'Host': '1234567890.execute-api.us-east-1.amazonaws.com',
+            },
+            'pathParameters': {
+                'proxy': 'return/request/url'
+            },
+            'httpMethod': 'GET',
+            'stageVariables': {},
+            'path': '/return/request/url'
+        }
+        response = lh.handler(event, None)
+
+        self.assertEqual(response['statusCode'], 200)
+        self.assertEqual(
+            response['body'],
+            'https://1234567890.execute-api.us-east-1.amazonaws.com/dev/return/request/url'
+        )
+
+    def test_wsgi_script_name_on_domain_url(self):
+        """
+        Ensure that requests to the amazonaws.com host for an API with a
+        domain have the correct request.url
+        """
+        lh = LambdaHandler('tests.test_wsgi_script_name_settings')
+
+        event = {
+            'body': '',
+            'resource': '/{proxy+}',
+            'requestContext': {},
+            'queryStringParameters': {},
+            'headers': {
+                'Host': 'example.com',
+            },
+            'pathParameters': {
+                'proxy': 'return/request/url'
+            },
+            'httpMethod': 'GET',
+            'stageVariables': {},
+            'path': '/return/request/url'
+        }
+        response = lh.handler(event, None)
+
+        self.assertEqual(response['statusCode'], 200)
+        self.assertEqual(
+            response['body'],
+            'https://example.com/return/request/url'
+        )
+
+    def test_wsgi_script_name_on_test_request(self):
+        """
+        Ensure that requests sent by the "Send test request" button behaves
+        sensibly
+        """
+        lh = LambdaHandler('tests.test_wsgi_script_name_settings')
+
+        event = {
+            'body': '',
+            'resource': '/{proxy+}',
+            'requestContext': {},
+            'queryStringParameters': {},
+            'headers': {},
+            'pathParameters': {
+                'proxy': 'return/request/url'
+            },
+            'httpMethod': 'GET',
+            'stageVariables': {},
+            'path': '/return/request/url'
+        }
+        response = lh.handler(event, None)
+
+        self.assertEqual(response['statusCode'], 200)
+        self.assertEqual(
+            response['body'],
+            'https://zappa:80/return/request/url'
+        )
diff --git a/tests/test_wsgi_script_name_app.py b/tests/test_wsgi_script_name_app.py
new file mode 100644
--- /dev/null
+++ b/tests/test_wsgi_script_name_app.py
@@ -0,0 +1,8 @@
+from flask import Flask, request
+
+app = Flask(__name__)
+
+
+@app.route('/return/request/url', methods=['GET', 'POST'])
+def return_request_url():
+    return request.url
diff --git a/tests/test_wsgi_script_name_settings.py b/tests/test_wsgi_script_name_settings.py
new file mode 100644
--- /dev/null
+++ b/tests/test_wsgi_script_name_settings.py
@@ -0,0 +1,11 @@
+API_STAGE = 'dev'
+APP_FUNCTION = 'app'
+APP_MODULE = 'tests.test_wsgi_script_name_app'
+BINARY_SUPPORT = False
+CONTEXT_HEADER_MAPPINGS = {}
+DEBUG = 'True'
+DJANGO_SETTINGS = None
+DOMAIN = 'api.example.com'
+ENVIRONMENT_VARIABLES = {}
+LOG_LEVEL = 'DEBUG'
+PROJECT_NAME = 'wsgi_script_name_settings'