Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

[BUG] HttpMaid with route config for GET / combined with template.yml function config Path: / SHOULD work with sam local start-api #86

Open
lestephane opened this issue Jun 6, 2020 · 4 comments
Assignees
Labels
bug Something isn't working

Comments

@lestephane
Copy link

lestephane commented Jun 6, 2020

Describe the bug
If i'm a first time user doing something that feels like it should work, it should work.

In this example, the java code configures the / route, and the template.yml mounts the
lambda onto /. Upon invocation, it fails because of AwsLambdaEndpoint's expectation
that there absolutely be a path parameter named "path":

AwsLambdaEndpoint.java:59

final Map<String, String> pathParameters = event.getPathParameters();
final String path = pathParameters.get("path");  // NullPointerException here
  • using /{path+} does not help (see first comment)
  • using a deeper static path such as /hello does not help (see second comment)

HttpMaid should play nice with sam local start-api, or at the very least, we need to have one working workaround. This will otherwise trip first time users, who initially want to try their function locally without creating an AWS Lambda first.

To Reproduce
Main.java

public final class Main implements RequestHandler<APIGatewayProxyRequestEvent, APIGatewayProxyResponseEvent> {
  private static final AwsLambdaEndpoint ADAPTER = awsLambdaEndpointFor(httpMaidConfig());

  @Override
  public APIGatewayProxyResponseEvent handleRequest(APIGatewayProxyRequestEvent request, Context ctx) {
    return ADAPTER.delegate(request, ctx);
  }

  public static void main(String[] args) {
    final int port = 8080;
    final HttpMaid httpMaid = httpMaidConfig();
    final QuantumMaid quantumMaid = QuantumMaid.quantumMaid()
        .withHttpMaid(httpMaid)
        .withLocalHostEndpointOnPort(port);
    quantumMaid.runAsynchronously();
  }

  private static HttpMaid httpMaidConfig() {
    final HttpMaid httpMaid = HttpMaid.anHttpMaid()
        .get("/", (request, response) -> response.setBody("Hello World!"))
        .build();
    return httpMaid;
  }
}

template.yml

Resources:
  HelloWorldFunction:
    Type: AWS::Serverless::Function
    Properties:
      CodeUri: .
      Handler: de.quantummaid.tutorials.Main::handleRequest
      Runtime: java11
      MemorySize: 512
      Events:
        HelloWorld:
          Type: Api
          Properties:
            Path: /
            Method: get
$ curl -vS localhost:3000
* Rebuilt URL to: localhost:3000/
*   Trying 127.0.0.1...
* Connected to localhost (127.0.0.1) port 3000 (#0)
> GET / HTTP/1.1
> Host: localhost:3000
> User-Agent: curl/7.47.0
> Accept: */*
> 
* HTTP 1.0, assume close after body
< HTTP/1.0 502 BAD GATEWAY
< Content-Type: application/json
< Content-Length: 36
< Server: Werkzeug/0.16.0 Python/3.7.4
< Date: Sat, 06 Jun 2020 10:47:50 GMT
< 
{"message":"Internal server error"}
* Closing connection 0

sam local

$ sam local start-api --skip-pull-image
Mounting HelloWorldFunction at http://127.0.0.1:3000/ [GET]
You can now browse to the above endpoints to invoke your functions. You do not need to restart/reload SAM CLI while working on your functions, changes will be reflected instantly/automatically. You only need to restart SAM CLI if you update your AWS SAM template
2020-06-06 13:47:42  * Running on http://127.0.0.1:3000/ (Press CTRL+C to quit)
Invoking de.quantummaid.tutorials.Main::handleRequest (java11)
Requested to skip pulling images ...

Mounting /home/lestephane/GitRepos/quantummaid-tutorials/aws-lambda/step3/.aws-sam/build/HelloWorldFunction as /var/task:ro,delegated inside runtime container
START RequestId: 0d2b3e27-d10a-1f13-fb78-ae306620dc8a Version: $LATEST
10:47:50.182 [main] ERROR de.quantummaid.httpmaid.HttpMaid - Exception in endpoint request handling
java.lang.NullPointerException: null
	at de.quantummaid.httpmaid.awslambda.AwsLambdaEndpoint.lambda$delegate$0(AwsLambdaEndpoint.java:59)
	at de.quantummaid.httpmaid.HttpMaid.handleRequest(HttpMaid.java:77)
	at de.quantummaid.httpmaid.HttpMaid.handleRequestSynchronously(HttpMaid.java:66)
	at de.quantummaid.httpmaid.awslambda.AwsLambdaEndpoint.delegate(AwsLambdaEndpoint.java:54)
	at de.quantummaid.tutorials.Main.handleRequest(Main.java:18)
	at java.base/jdk.internal.reflect.NativeMethodAccessorImpl.invoke0(Native Method)
	at java.base/jdk.internal.reflect.NativeMethodAccessorImpl.invoke(Unknown Source)
	at java.base/jdk.internal.reflect.DelegatingMethodAccessorImpl.invoke(Unknown Source)
	at java.base/java.lang.reflect.Method.invoke(Unknown Source)
	at lambdainternal.EventHandlerLoader$PojoMethodRequestHandler.handleRequest(EventHandlerLoader.java:280)
	at lambdainternal.EventHandlerLoader$PojoHandlerAsStreamHandler.handleRequest(EventHandlerLoader.java:197)
	at lambdainternal.EventHandlerLoader$2.call(EventHandlerLoader.java:897)
	at lambdainternal.AWSLambda.startRuntime(AWSLambda.java:228)
	at lambdainternal.AWSLambda.startRuntime(AWSLambda.java:162)
	at lambdainternal.AWSLambda.main(AWSLambda.java:157)
object must not be null: de.quantummaid.httpmaid.util.CustomTypeValidationException
de.quantummaid.httpmaid.util.CustomTypeValidationException: object must not be null
	at de.quantummaid.httpmaid.util.CustomTypeValidationException.customTypeValidationException(CustomTypeValidationException.java:32)
	at de.quantummaid.httpmaid.util.Validators.validateNotNull(Validators.java:33)
	at de.quantummaid.httpmaid.endpoint.SynchronizationWrapper.getObject(SynchronizationWrapper.java:42)
	at de.quantummaid.httpmaid.HttpMaid.handleRequestSynchronously(HttpMaid.java:70)
	at de.quantummaid.httpmaid.awslambda.AwsLambdaEndpoint.delegate(AwsLambdaEndpoint.java:54)
	at de.quantummaid.tutorials.Main.handleRequest(Main.java:18)
	at java.base/jdk.internal.reflect.NativeMethodAccessorImpl.invoke0(Native Method)
	at java.base/jdk.internal.reflect.NativeMethodAccessorImpl.invoke(Unknown Source)
	at java.base/jdk.internal.reflect.DelegatingMethodAccessorImpl.invoke(Unknown Source)
	at java.base/java.lang.reflect.Method.invoke(Unknown Source)

END RequestId: 0d2b3e27-d10a-1f13-fb78-ae306620dc8a
REPORT RequestId: 0d2b3e27-d10a-1f13-fb78-ae306620dc8a	Init Duration: 1286.63 ms	Duration: 52.19 ms	Billed Duration: 100 ms	Memory Size: 512 MB	Max Memory Used: 86 MB	
Lambda returned empty body!
Invalid API Gateway Response Keys: {'errorType', 'stackTrace', 'errorMessage'} in {'errorType': 'de.quantummaid.httpmaid.util.CustomTypeValidationException', 'errorMessage': 'object must not be null', 'stackTrace': ['de.quantummaid.httpmaid.util.CustomTypeValidationException.customTypeValidationException(CustomTypeValidationException.java:32)', 'de.quantummaid.httpmaid.util.Validators.validateNotNull(Validators.java:33)', 'de.quantummaid.httpmaid.endpoint.SynchronizationWrapper.getObject(SynchronizationWrapper.java:42)', 'de.quantummaid.httpmaid.HttpMaid.handleRequestSynchronously(HttpMaid.java:70)', 'de.quantummaid.httpmaid.awslambda.AwsLambdaEndpoint.delegate(AwsLambdaEndpoint.java:54)', 'de.quantummaid.tutorials.Main.handleRequest(Main.java:18)', 'java.base/jdk.internal.reflect.NativeMethodAccessorImpl.invoke0(Native Method)', 'java.base/jdk.internal.reflect.NativeMethodAccessorImpl.invoke(Unknown Source)', 'java.base/jdk.internal.reflect.DelegatingMethodAccessorImpl.invoke(Unknown Source)', 'java.base/java.lang.reflect.Method.invoke(Unknown Source)']}
<class 'samcli.local.apigw.local_apigw_service.LambdaResponseParseException'>
2020-06-06 13:47:50 127.0.0.1 - - [06/Jun/2020 13:47:50] "GET / HTTP/1.1" 502 -

Expected behavior
No error, HTTP 200 response with Hello World

Environment description

  • project version used:
        <dependency>
            <groupId>de.quantummaid.quantummaid.packagings</groupId>
            <artifactId>quantummaid-essentials</artifactId>
            <version>1.0.50</version>
        </dependency>
        <dependency>
            <groupId>de.quantummaid.httpmaid.integrations</groupId>
            <artifactId>httpmaid-awslambda</artifactId>
            <version>0.9.61</version>
        </dependency>
  • Java version used: 11

Additional context
Workaround this problem by adding /{path+}.

@lestephane lestephane added the bug Something isn't working label Jun 6, 2020
@lestephane
Copy link
Author

hmm even using /{path+} does not work, at least as far as sam local is concerned:
curl

$ curl -vS http://localhost:3000/
*   Trying 127.0.0.1...
* Connected to localhost (127.0.0.1) port 3000 (#0)
> GET / HTTP/1.1
> Host: localhost:3000
> User-Agent: curl/7.47.0
> Accept: */*
> 
* HTTP 1.0, assume close after body
< HTTP/1.0 403 FORBIDDEN
< Content-Type: application/json
< Content-Length: 43
< Server: Werkzeug/0.16.0 Python/3.7.4
< Date: Sat, 06 Jun 2020 10:54:56 GMT
< 
{"message":"Missing Authentication Token"}
* Closing connection 0

sam local

$ sam local start-api --skip-pull-image
Mounting HelloWorldFunction at http://127.0.0.1:3000/{path+} [GET]
You can now browse to the above endpoints to invoke your functions. You do not need to restart/reload SAM CLI while working on your functions, changes will be reflected instantly/automatically. You only need to restart SAM CLI if you update your AWS SAM template
2020-06-06 13:54:30  * Running on http://127.0.0.1:3000/ (Press CTRL+C to quit)
2020-06-06 13:54:32 127.0.0.1 - - [06/Jun/2020 13:54:32] "GET / HTTP/1.1" 403 -

@lestephane
Copy link
Author

hmm even using a subpath in the httpmaid config (like /hello) does not work.

Main.java

public final class Main implements RequestHandler<APIGatewayProxyRequestEvent, APIGatewayProxyResponseEvent> {
  private static final AwsLambdaEndpoint ADAPTER = awsLambdaEndpointFor(httpMaidConfig());

  @Override
  public APIGatewayProxyResponseEvent handleRequest(APIGatewayProxyRequestEvent request, Context ctx) {
    return ADAPTER.delegate(request, ctx);
  }

  public static void main(String[] args) {
    final int port = 8080;
    final HttpMaid httpMaid = httpMaidConfig();
    final QuantumMaid quantumMaid = QuantumMaid.quantumMaid()
        .withHttpMaid(httpMaid)
        .withLocalHostEndpointOnPort(port);
    quantumMaid.runAsynchronously();
  }

  private static HttpMaid httpMaidConfig() {
    final HttpMaid httpMaid = HttpMaid.anHttpMaid()
        .get("/hello", (request, response) -> response.setBody("Hello World!"))
        .build();
    return httpMaid;
  }
}

curl

$ curl -vS http://localhost:3000/hello
*   Trying 127.0.0.1...
* Connected to localhost (127.0.0.1) port 3000 (#0)
> GET /hello HTTP/1.1
> Host: localhost:3000
> User-Agent: curl/7.47.0
> Accept: */*
> 
* HTTP 1.0, assume close after body
< HTTP/1.0 404 NOT FOUND
< Content-Type: application/json
< Content-Length: 0
< Server: Werkzeug/0.16.0 Python/3.7.4
< Date: Sat, 06 Jun 2020 11:01:06 GMT
< 
* Closing connection 0

sam local

$ sam local start-api --skip-pull-image
Mounting HelloWorldFunction at http://127.0.0.1:3000/{path+} [GET]
You can now browse to the above endpoints to invoke your functions. You do not need to restart/reload SAM CLI while working on your functions, changes will be reflected instantly/automatically. You only need to restart SAM CLI if you update your AWS SAM template
2020-06-06 14:00:52  * Running on http://127.0.0.1:3000/ (Press CTRL+C to quit)
Invoking de.quantummaid.tutorials.Main::handleRequest (java11)
Requested to skip pulling images ...

Mounting /home/lestephane/GitRepos/quantummaid-tutorials/aws-lambda/step3/.aws-sam/build/HelloWorldFunction as /var/task:ro,delegated inside runtime container
START RequestId: 4e8557fa-51ec-1ce6-59b3-4b6e94892c67 Version: $LATEST
11:01:05.702 [main] DEBUG de.quantummaid.httpmaid.exceptions.HandlerExceptionMapper - Handled exception during HttpMaid request processing
de.quantummaid.httpmaid.handler.PageNotFoundException: No handler found for path 'hello' and method 'GET'

PATH = Path(path=hello)
QUERY_PARAMETERS = QueryParameters(queryParameters={})
RESPONSE_STATUS = 200
REQUEST_BODY_STRING = 
UNMARSHALLED_REQUEST_BODY = null
RAW_METHOD = GET
IS_HTTP_REQUEST = true
REQUEST_CONTENT_TYPE = ContentType(value=null)
RESPONSE_HEADERS = {}
REQUEST_BODY_STREAM = java.io.ByteArrayInputStream@782859e
RAW_PATH = hello
RAW_REQUEST_QUERY_PARAMETERS = {}
METHOD = HttpRequestMethod(value=GET)
RAW_REQUEST_HEADERS = {Accept=[*/*], User-Agent=[curl/7.47.0], X-Forwarded-Proto=[http], Host=[localhost:3000], X-Forwarded-Port=[3000]}
REQUEST_HEADERS = Headers(headers={HeaderKey(value=user-agent)=HeaderValue(value=curl/7.47.0), HeaderKey(value=host)=HeaderValue(value=localhost:3000), HeaderKey(value=accept)=HeaderValue(value=*/*), HeaderKey(value=x-forwarded-proto)=HeaderValue(value=http), HeaderKey(value=x-forwarded-port)=HeaderValue(value=3000)})
awsLambdaContext = lambdainternal.api.LambdaContext@23f5b5dc
	at de.quantummaid.httpmaid.handler.PageNotFoundException.pageNotFoundException(PageNotFoundException.java:41)
	at de.quantummaid.httpmaid.handler.InvokeHandlerProcessor.lambda$apply$0(InvokeHandlerProcessor.java:45)
	at java.base/java.util.Optional.orElseThrow(Unknown Source)
	at de.quantummaid.httpmaid.handler.InvokeHandlerProcessor.apply(InvokeHandlerProcessor.java:45)
	at de.quantummaid.httpmaid.chains.Chain.lambda$accept$0(Chain.java:74)
	at java.base/java.util.stream.ForEachOps$ForEachOp$OfRef.accept(Unknown Source)
	at java.base/java.util.stream.ReferencePipeline$3$1.accept(Unknown Source)
	at java.base/java.util.LinkedList$LLSpliterator.forEachRemaining(Unknown Source)
	at java.base/java.util.stream.AbstractPipeline.copyInto(Unknown Source)
	at java.base/java.util.stream.AbstractPipeline.wrapAndCopyInto(Unknown Source)
	at java.base/java.util.stream.ForEachOps$ForEachOp.evaluateSequential(Unknown Source)
	at java.base/java.util.stream.ForEachOps$ForEachOp$OfRef.evaluateSequential(Unknown Source)
	at java.base/java.util.stream.AbstractPipeline.evaluate(Unknown Source)
	at java.base/java.util.stream.ReferencePipeline.forEach(Unknown Source)
	at de.quantummaid.httpmaid.chains.Chain.accept(Chain.java:74)
	at de.quantummaid.httpmaid.chains.ChainRegistry.accept(ChainRegistry.java:69)
	at de.quantummaid.httpmaid.chains.ChainRegistry.handleAction(ChainRegistry.java:78)
	at de.quantummaid.httpmaid.chains.ChainRegistry.accept(ChainRegistry.java:70)
	at de.quantummaid.httpmaid.chains.ChainRegistry.handleAction(ChainRegistry.java:78)
	at de.quantummaid.httpmaid.chains.ChainRegistry.accept(ChainRegistry.java:70)
	at de.quantummaid.httpmaid.chains.ChainRegistry.handleAction(ChainRegistry.java:78)
	at de.quantummaid.httpmaid.chains.ChainRegistry.accept(ChainRegistry.java:70)
	at de.quantummaid.httpmaid.chains.ChainRegistry.handleAction(ChainRegistry.java:78)
	at de.quantummaid.httpmaid.chains.ChainRegistry.accept(ChainRegistry.java:70)
	at de.quantummaid.httpmaid.chains.ChainRegistry.handleAction(ChainRegistry.java:78)
	at de.quantummaid.httpmaid.chains.ChainRegistry.accept(ChainRegistry.java:70)
	at de.quantummaid.httpmaid.chains.ChainRegistry.handleAction(ChainRegistry.java:78)
	at de.quantummaid.httpmaid.chains.ChainRegistry.accept(ChainRegistry.java:70)
	at de.quantummaid.httpmaid.chains.ChainRegistry.handleAction(ChainRegistry.java:78)
	at de.quantummaid.httpmaid.chains.ChainRegistry.accept(ChainRegistry.java:70)
	at de.quantummaid.httpmaid.chains.ChainRegistry.putIntoChain(ChainRegistry.java:63)
	at de.quantummaid.httpmaid.HttpMaid.handleRequest(HttpMaid.java:85)
	at de.quantummaid.httpmaid.HttpMaid.handleRequestSynchronously(HttpMaid.java:66)
	at de.quantummaid.httpmaid.awslambda.AwsLambdaEndpoint.delegate(AwsLambdaEndpoint.java:54)
	at de.quantummaid.tutorials.Main.handleRequest(Main.java:18)
	at java.base/jdk.internal.reflect.NativeMethodAccessorImpl.invoke0(Native Method)
	at java.base/jdk.internal.reflect.NativeMethodAccessorImpl.invoke(Unknown Source)
	at java.base/jdk.internal.reflect.DelegatingMethodAccessorImpl.invoke(Unknown Source)
	at java.base/java.lang.reflect.Method.invoke(Unknown Source)
	at lambdainternal.EventHandlerLoader$PojoMethodRequestHandler.handleRequest(EventHandlerLoader.java:280)
	at lambdainternal.EventHandlerLoader$PojoHandlerAsStreamHandler.handleRequest(EventHandlerLoader.java:197)
	at lambdainternal.EventHandlerLoader$2.call(EventHandlerLoader.java:897)
	at lambdainternal.AWSLambda.startRuntime(AWSLambda.java:228)
	at lambdainternal.AWSLambda.startRuntime(AWSLambda.java:162)
	at lambdainternal.AWSLambda.main(AWSLambda.java:157)
END RequestId: 4e8557fa-51ec-1ce6-59b3-4b6e94892c67
REPORT RequestId: 4e8557fa-51ec-1ce6-59b3-4b6e94892c67	Init Duration: 1278.64 ms	Duration: 74.37 ms	Billed Duration: 100 ms	Memory Size: 512 MB	Max Memory Used: 85 MB	
No Content-Type given. Defaulting to 'application/json'.
2020-06-06 14:01:06 127.0.0.1 - - [06/Jun/2020 14:01:06] "GET /hello HTTP/1.1" 404 -

@lestephane lestephane changed the title [BUG] HttpMaid relies on {path+} being always present, NPE when it isn't [BUG] HttpMaid with route config for GET / combined with template.yml function config Path: / SHOULD work with sam local start-api Jun 6, 2020
@lestephane
Copy link
Author

The workaround is currently:

  • configure httpmaid to respond to a request to a deeper path (/hello) AND
  • use /{path+} in template.yml.

There is a 99% chance first time users will fail, even if i put a large warning in the tutorial, because they want to go straight to the goodies, and /{path+} is not intuitive.

The goal should remain that, in addition to the path parameter being optional, simple one-to-one mappings should work, such as for example:

Main.java

private static HttpMaid httpMaidConfig() {
  final HttpMaid httpMaid = HttpMaid.anHttpMaid()
      .get("/", (request, response) -> response.setBody("Hello from /"))
      .get("/hello", (request, response) -> response.setBody("Hello from /hello"))
      .build();
  return httpMaid;
}

template.yml

Events:
  HelloFromFixedPathRoot:
    Type: Api
    Properties:
      Path: /
      Method: get
  HelloFromFixedPathHello:
    Type: Api
    Properties:
      Path: /hello
      Method: get

@lestephane
Copy link
Author

lestephane commented Jun 7, 2020

The case of mapping / may be impossible for now because of a SAM CLI limitation:
aws/aws-sam-cli#437
This leaves us with one-to-one anything-but-/ static mappings which need to work.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
bug Something isn't working
Projects
None yet
Development

No branches or pull requests

2 participants