Repository: localstack/localstack

Issue:
Unable to find forwarding rule for host - SQS
# Type of request: This is a ...
[x ] bug report
[ ] feature request

# Detailed description
When attempting to run in a Docker environment with Spring Cloud AWS Messaging - getting a 404 on the application side and `INFO:localstack.services.edge: Unable to find forwarding rule for host "awslocal:4566", path "/", target header "", auth header ""` on Localstack container. 

Using the following docker-compose entry:

```
localstack:
    image: localstack/localstack:0.11.2
    container_name: awslocal
    hostname: awslocal
    environment:
      SERVICES: "sqs,sns"
      HOST_TMP_FOLDER: /tmp/localstack
      HOSTNAME_EXTERNAL: awslocal
    ports:
      - "4566:4566"
      - "8055:8080"
    volumes:
      - "./.localstack:/tmp/localstack"
```

## Expected behavior
Application can listen to SQS messages on the `test` queue created externally with AWS CLI:

```
aws --endpoint-url=http://localhost:4566 sqs create-queue --queue-name test
{
    "QueueUrl": "http://localhost:4566/queue/test"
}

aws --endpoint-url=http://localhost:4566 sns create-topic --name test
{
    "TopicArn": "arn:aws:sns:us-east-1:000000000000:test"
}

aws --endpoint-url=http://localhost:4566 sns subscribe --topic-arn arn:aws:sns:us-east-1:000000000000:test --protocol sqs --notification-endpoint arn:aws:sqs:us-east-1:000000000000:test
{
    "SubscriptionArn": "arn:aws:sns:us-east-1:000000000000:test:de195743-b616-4c13-8539-fe17d8e00084"
}

aws --endpoint-url=http://localhost:4566 sns publish --topic-arn arn:aws:sns:us-east-1:000000000000:test --message "TEST"
```

## Actual behavior
Application fails with a 404 on this log line:

```
app_1         |  2020-05-30 01:02:36.955 DEBUG 6 --- [           main] com.amazonaws.request                    : Sending Request: POST http://awslocal:4566 / Parameters: ({"Action":["GetQueueUrl"],"Version":["2012-11-05"],"QueueName":["test"]}Headers: (User-Agent: aws-sdk-java/1.11.415 Linux/4.19.76-linuxkit OpenJDK_64-Bit_Server_VM/11.0.7+10 java/11.0.7, amz-sdk-invocation-id: 1ae4996f-7f4e-509a-0ee1-a5a7d7d274a0, )
app_1         | 2020-05-30 01:02:37.214 DEBUG 6 --- [           main] com.amazonaws.request                    : Received error response: com.amazonaws.services.sqs.model.AmazonSQSException: null (Service: AmazonSQS; Status Code: 404; Error Code: 404 Not Found; Request ID: null)
app_1         |  2020-05-30 01:02:37.220  WARN 6 --- [           main] ConfigServletWebServerApplicationContext : Exception encountered during context initialization - cancelling refresh attempt: org.springframework.beans.factory.BeanCreationException: Error creating bean with name 'simpleMessageListenerContainer' defined in class path resource [org/springframework/cloud/aws/messaging/config/annotation/SqsConfiguration.class]: Invocation of init method failed; nested exception is com.amazonaws.services.sqs.model.AmazonSQSException: null (Service: AmazonSQS; Status Code: 404; Error Code: 404 Not Found; Request ID: null)
app_1         |  2020-05-30 01:02:37.224  INFO 6 --- [           main] j.LocalContainerEntityManagerFactoryBean : Closing JPA EntityManagerFactory for persistence unit 'default'
app_1         |  2020-05-30 01:02:37.225  INFO 6 --- [           main] .SchemaDropperImpl$DelayedDropActionImpl : HHH000477: Starting delayed evictData of schema as part of SessionFactory shut-down'
app_1         |  2020-05-30 01:02:37.233  INFO 6 --- [           main] o.s.s.concurrent.ThreadPoolTaskExecutor  : Shutting down ExecutorService 'applicationTaskExecutor'
app_1         |  2020-05-30 01:02:37.235  INFO 6 --- [           main] com.zaxxer.hikari.HikariDataSource       : HikariPool-1 - Shutdown initiated...
app_1         |  2020-05-30 01:02:37.249  INFO 6 --- [           main] com.zaxxer.hikari.HikariDataSource       : HikariPool-1 - Shutdown completed.
app_1         |  2020-05-30 01:02:37.253  INFO 6 --- [           main] o.apache.catalina.core.StandardService   : Stopping service [Tomcat]
app_1         |  2020-05-30 01:02:37.271  INFO 6 --- [           main] ConditionEvaluationReportLoggingListener :
app_1         |
app_1         | Error starting ApplicationContext. To display the conditions report re-run your application with 'debug' enabled.
app_1         |  2020-05-30 01:02:37.278 ERROR 6 --- [           main] o.s.boot.SpringApplication               : Application run failed
app_1         |  com.amazonaws.services.sqs.model.AmazonSQSException: null (Service: AmazonSQS; Status Code: 404; Error Code: 404 Not Found; Request ID: null)
app_1         | 	at com.amazonaws.http.AmazonHttpClient$RequestExecutor.handleErrorResponse(AmazonHttpClient.java:1660)
app_1         | 	at com.amazonaws.http.AmazonHttpClient$RequestExecutor.executeOneRequest(AmazonHttpClient.java:1324)
app_1         | 	at com.amazonaws.http.AmazonHttpClient$RequestExecutor.executeHelper(AmazonHttpClient.java:1074)
app_1         | 	at com.amazonaws.http.AmazonHttpClient$RequestExecutor.doExecute(AmazonHttpClient.java:745)
app_1         | 	at com.amazonaws.http.AmazonHttpClient$RequestExecutor.executeWithTimer(AmazonHttpClient.java:719)
app_1         | 	at com.amazonaws.http.AmazonHttpClient$RequestExecutor.execute(AmazonHttpClient.java:701)
app_1         | 	at com.amazonaws.http.AmazonHttpClient$RequestExecutor.access$500(AmazonHttpClient.java:669)
app_1         | 	at com.amazonaws.http.AmazonHttpClient$RequestExecutionBuilderImpl.execute(AmazonHttpClient.java:651)
app_1         | 	at com.amazonaws.http.AmazonHttpClient.execute(AmazonHttpClient.java:515)
app_1         | 	at com.amazonaws.services.sqs.AmazonSQSClient.doInvoke(AmazonSQSClient.java:2147)
app_1         | 	at com.amazonaws.services.sqs.AmazonSQSClient.invoke(AmazonSQSClient.java:2116)
app_1         | 	at com.amazonaws.services.sqs.AmazonSQSClient.invoke(AmazonSQSClient.java:2105)
app_1         | 	at com.amazonaws.services.sqs.AmazonSQSClient.executeGetQueueUrl(AmazonSQSClient.java:1138)
app_1         | 	at com.amazonaws.services.sqs.AmazonSQSClient.getQueueUrl(AmazonSQSClient.java:1110)
```

# Steps to reproduce
A demo repository is here: https://github.com/MatthewEdge/boot-sqs-test

Relevant client code:

```
@SpringBootApplication
@Slf4j
public class DemoApplication {

  public static void main(String[] args) {
    SpringApplication.run(DemoApplication.class, args);
  }

  @SqsListener("test")
  public void receiveMessage(@NotificationMessage String message, @Payload String payload) {
    log.info(payload);
    log.info(message);
  }

  @Value("${cloud.aws.endpoint.uri}")
  private String endpointUrl;

  @Value("${cloud.aws.region.static}")
  private String region;

  private AWSCredentialsProvider credentialsProvider() {
      return new AWSStaticCredentialsProvider(new AnonymousAWSCredentials());
  }

  private EndpointConfiguration endpointConfiguration() {
    log.info("Using endpoint: " + endpointUrl);
    log.info("Region: " + region);
    return new AwsClientBuilder.EndpointConfiguration(endpointUrl, region);
  }

  @Bean
  public AmazonSQS amazonSQS() {
    return AmazonSQSAsyncClientBuilder.standard()
        .withCredentials(credentialsProvider())
        .withEndpointConfiguration(endpointConfiguration())
        .build();
  }
}
```

## Command used to start LocalStack
```
docker-compose up -d localstack
docker-compose up --build app
```



┆Issue is synchronized with this [Jira Bug](https://localstack.atlassian.net/browse/LOC-190) by [Unito](https://www.unito.io/learn-more)



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
@@ -38,21 +38,7 @@ def forward_request(self, method, path, data, headers):
             return 404
 
         if not port:
-            # detect S3 presigned URLs
-            if 'AWSAccessKeyId=' in path or 'Signature=' in path:
-                port = config.PORT_S3
-            # TODO: move S3 public URLs to a separate port/endpoint, OR check ACLs here first
-            stripped = path.strip('/')
-            if method == 'GET' and '/' in stripped:
-                # assume that this is an S3 GET request with URL path `/<bucket>/<key ...>`
-                port = config.PORT_S3
-            if stripped and '/' not in stripped:
-                if method == 'PUT':
-                    # assume that this is an S3 PUT bucket request with URL path `/<bucket>`
-                    port = config.PORT_S3
-                elif method == 'POST' and to_bytes('key=') in to_bytes(data or ''):
-                    # assume that this is an S3 POST request with form parameters in the body
-                    port = config.PORT_S3
+            port = get_port_from_custom_rules(method, path, data, headers) or port
 
         if not port:
             if api in ['', None, '_unknown_']:
@@ -80,6 +66,8 @@ def forward_request(self, method, path, data, headers):
 
 
 def get_api_from_headers(headers, path=None):
+    """ Determine API and backend port based on Authorization headers. """
+
     target = headers.get('x-amz-target', '')
     host = headers.get('host', '')
     auth_header = headers.get('authorization', '')
@@ -123,6 +111,31 @@ def get_api_from_headers(headers, path=None):
     return result[0], result_before[1] or result[1], path, host
 
 
+def get_port_from_custom_rules(method, path, data, headers):
+    """ Determine backend port based on custom rules. """
+
+    # detect S3 presigned URLs
+    if 'AWSAccessKeyId=' in path or 'Signature=' in path:
+        return config.PORT_S3
+
+    # 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:
+        # assume that this is an S3 GET request with URL path `/<bucket>/<key ...>`
+        return config.PORT_S3
+    if stripped and '/' not in stripped:
+        if method == 'PUT':
+            # assume that this is an S3 PUT bucket request with URL path `/<bucket>`
+            return config.PORT_S3
+        if method == 'POST' and to_bytes('key=') in data_bytes:
+            # assume that this is an S3 POST request with form parameters 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
     return config.service_port(service)


Test patch: