Repository: localstack/localstack

Issue:
HEAD request is not properly forwarded to S3 through edge router
<!-- Love localstack? Please consider supporting our collective:
👉  https://opencollective.com/localstack/donate -->

# Type of request: This is a ...

- [x] bug report
- [ ] feature request

# Detailed description
I get a 404 when I request a file I uploaded via a HEAD request.
This only happens when using the new edge router port.
I think there's faulty logic in the edge router when looking at HEAD requests.

## Expected behavior

I expect the behavior I get from requesting via the dedicated S3 port (HTTP 200 if a file exists)

## Actual behavior

A 404 on HEAD request through the edge router port.

# Steps to reproduce
```
version: '2.2'

services:
  localstack:
    image: localstack/localstack
    ports:
      - '4566:4566'
    environment:
      - SERVICES=s3
      - HOSTNAME_EXTERNAL=s3.test

```
```
docker-compose up -d
```
Making a bucket and uploading a file:
```
AWS_ENDPOINT_URL=http://s3.test:4566
aws --endpoint-url=$AWS_ENDPOINT_URL s3 mb s3://attachments
aws --endpoint-url=$AWS_ENDPOINT_URL s3 cp test s3://attachments
```
Lastly, HEADing the file to see if it exists.
```
$ curl -I $AWS_ENDPOINT_URL/attachments/test -H 'Authorization: aaa'
HTTP/1.1 404 Not Found
Server: BaseHTTP/0.6 Python/3.8.2
Date: Wed, 03 Jun 2020 21:28:46 GMT
Content-Length: 21
Access-Control-Allow-Origin: *
Access-Control-Allow-Methods: HEAD,GET,PUT,POST,DELETE,OPTIONS,PATCH
Access-Control-Allow-Headers: authorization,content-type,content-md5,cache-control,x-amz-content-sha256,x-amz-date,x-amz-security-token,x-amz-user-agent,x-amz-target,x-amz-acl,x-amz-version-id,x-localstack-target,x-amz-tagging
Access-Control-Expose-Headers: x-amz-version-id
```
Localstack logs from requesting via *:4566
```
localstack_1  | Starting edge router (https port 4566)...
localstack_1  | Starting mock S3 service in http ports 4566 (recommended) and 4572 (deprecated)...
localstack_1  | 2020-06-03T21:04:36:INFO:localstack.multiserver: Starting multi API server process on port 38679
localstack_1  | Ready.
localstack_1  | 2020-06-03T21:04:41:INFO:localstack.utils.persistence: Restored 10 API calls from persistent file: /tmp/localstack/recorded_api_calls.json
localstack_1  | 2020-06-03T21:05:09:INFO:localstack.services.edge: Unable to find forwarding rule for host "s3.test:4566", path "/attachments/test", target header "", auth header ""
```

Switching the exposed port to 4572 and requesting through there gives me:
```
AWS_ENDPOINT_URL=http://s3.test:4572
```
```
$ curl -I $AWS_ENDPOINT_URL/attachments/test -H 'Authorization: aaa'
HTTP/1.1 200 OK
Server: BaseHTTP/0.6 Python/3.8.2
Date: Wed, 03 Jun 2020 21:39:38 GMT
Content-Type: binary/octet-stream
ETag: "d41d8cd98f00b204e9800998ecf8427e"
last-modified: Wed, 03 Jun 2020 21:31:11 GMT
Content-Length: 0
Access-Control-Allow-Origin: *
x-amz-request-id: 3144F10D603C9424
x-amz-id-2: MzRISOwyjmnup3144F10D603C94247/JypPGXLh0OVFGcJaaO3KW/hRAqKOpIEEp
Access-Control-Allow-Methods: HEAD,GET,PUT,POST,DELETE,OPTIONS,PATCH
Access-Control-Allow-Headers: authorization,content-type,content-md5,cache-control,x-amz-content-sha256,x-amz-date,x-amz-security-token,x-amz-user-agent,x-amz-target,x-amz-acl,x-amz-version-id,x-localstack-target,x-amz-tagging
Access-Control-Expose-Headers: x-amz-version-id
```


Discussion:

Resolving patch:
diff --git a/localstack/services/edge.py b/localstack/services/edge.py
--- a/localstack/services/edge.py
+++ b/localstack/services/edge.py
@@ -133,10 +133,14 @@ def get_port_from_custom_rules(method, path, data, headers):
     if re.match(r'^/queue/[a-zA-Z0-9_-]+$', path):
         return config.PORT_SQS
 
+    data_bytes = to_bytes(data or '')
+
+    if path == '/' and to_bytes('QueueName=') in data_bytes:
+        return config.PORT_SQS
+
     # TODO: move S3 public URLs to a separate port/endpoint, OR check ACLs here first
     stripped = path.strip('/')
-    data_bytes = to_bytes(data or '')
-    if method == 'GET' and '/' in stripped:
+    if method in ['GET', 'HEAD'] and '/' in stripped:
         # assume that this is an S3 GET request with URL path `/<bucket>/<key ...>`
         return config.PORT_S3
 
@@ -148,9 +152,6 @@ def get_port_from_custom_rules(method, path, data, headers):
             # assume that this is an S3 POST request with form parameters or multipart form in the body
             return config.PORT_S3
 
-    if path == '/' and to_bytes('QueueName=') in data_bytes:
-        return config.PORT_SQS
-
 
 def get_service_port_for_account(service, headers):
     # assume we're only using a single account, hence return the static port mapping from config.py
diff --git a/localstack/services/s3/s3_listener.py b/localstack/services/s3/s3_listener.py
--- a/localstack/services/s3/s3_listener.py
+++ b/localstack/services/s3/s3_listener.py
@@ -508,6 +508,7 @@ def remove_xml_preamble(response):
 # HELPER METHODS
 #   for lifecycle/replication/encryption/...
 # --------------
+
 def get_lifecycle(bucket_name):
     bucket_name = normalize_bucket_name(bucket_name)
     exists, code, body = is_bucket_available(bucket_name)
@@ -640,6 +641,7 @@ def set_object_lock(bucket_name, lock_config):
 # -------------
 # UTIL METHODS
 # -------------
+
 def strip_chunk_signatures(data):
     # For clients that use streaming v4 authentication, the request contains chunk signatures
     # in the HTTP body (see example below) which we need to strip as moto cannot handle them
diff --git a/localstack/utils/common.py b/localstack/utils/common.py
--- a/localstack/utils/common.py
+++ b/localstack/utils/common.py
@@ -1131,7 +1131,7 @@ def clean_cache(file_pattern=CACHE_FILE_PATTERN,
 
 
 def truncate(data, max_length=100):
-    return ('%s...' % data[:max_length]) if len(data) > max_length else data
+    return ('%s...' % data[:max_length]) if len(data or '') > max_length else data
 
 
 def parallelize(func, list, size=None):


Test patch: