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

Problems with 403 Forbidden and 405 Method Not Allowed #17

Closed
davidmco65 opened this issue Mar 20, 2017 · 11 comments
Closed

Problems with 403 Forbidden and 405 Method Not Allowed #17

davidmco65 opened this issue Mar 20, 2017 · 11 comments

Comments

@davidmco65
Copy link

Hi,
I'm having an issue trying to get a single Java Lambda to handle all HTTP Methods.

Here's my interface:

public interface ContactResource
{
	@DELETE
	@Consumes({MediaType.APPLICATION_JSON})
	@Produces({MediaType.APPLICATION_JSON})
	@Path("/{id}")
	public Response deleteContact(@PathParam("id") String id);

	@POST
	@Consumes({MediaType.APPLICATION_JSON})
	@Produces({MediaType.APPLICATION_JSON})
	public Response createContact(ContactObject co);

	...
}

And my implementation class:

@Path("/contacts")
public class ContactResourceImpl implements ContactResource
{
	@Override
	public Response deleteContact(@PathParam("id") String id)
	{
		return deleteContact(id, null);
	}

	@Override
	public Response createContact(ContactObject co)
	{
		LOG.debug("Entering contacts::POST");
		try
		{
			...
		}
	}
}

Yet when I try to use Postman to call POST on /contacts (actually /TEST/contacts because TEST is my stage name), I get the 403 Forbidden error.

And when I try to use DELETE on /contacts/12345 - where I should receive a 404 Not Found because Contact with id 12345 doesn't exist - I get 405 Method Not Allowed. I also get a return header of "Allow={GET,OPTIONS,PUT}".

Any help would be greatly appreciated!

Thanks,
David

@sapessi
Copy link
Collaborator

sapessi commented Mar 20, 2017

Looking into it. Just to confirm, if you start this on your local machine using the embedded container in Jersey it responds correctly?

Could you also share a Swagger file with your API Gateway configuration (or a screenshot of the console?)

@sapessi
Copy link
Collaborator

sapessi commented Mar 20, 2017

I've tried to test something similar in local with Jersey 2.24. Looks like you cannot define a method with a sub-path in the interface, you have to define both in the implementation. Note that all of these tests are with the embedded Grizzly container in local. We'll get to Lambda and API Gateway next.

The DELETE method works for me when the interface is defined like this:

public interface ContactResource
{
    @DELETE // you could skip this, it's here just for clarity
    @Consumes({MediaType.APPLICATION_JSON})
    @Produces({MediaType.APPLICATION_JSON})
    public Response deleteContact(String id);

    @POST
    @Consumes({MediaType.APPLICATION_JSON})
    @Produces({MediaType.APPLICATION_JSON})
    public Response createContact(Map<String, String> co);
}

And the implementation looks like this

@Path("contacts")
public class ContactResourceImpl implements ContactResource {

    @DELETE
    @Path("{id}")
    public Response deleteContact(@PathParam("id") String id)
    {
        return Response.status(204).build();
    }

    public Response createContact(Map<String, String> co)
    {
        return Response.status(200).build();
    }
}

@davidmco65
Copy link
Author

So when I define in both Interface and Implementation, I get Duplicate definition errors. It thinks both the Interface and Implementation are definitions.

So I ran my JUnit tests, which I'm assuming fires up Grizzly. All tests worked. I've actually got more methods:

@HEAD
@Path("/ping")
@Consumes({MediaType.APPLICATION_JSON})
public Response ping(@QueryParam("numAdditionalInstancesToPing") int numAdditionalInstancesToPing);

@DELETE
@Consumes({MediaType.APPLICATION_JSON})
@Produces({MediaType.APPLICATION_JSON})
@Path("/{proxy}")
public Response deleteContact(@PathParam("proxy") String id);

@POST
@Consumes({MediaType.APPLICATION_JSON})
@Produces({MediaType.APPLICATION_JSON})
public Response createContact(ContactObject co);

@PUT
@Path("/{proxy}")
@Produces({MediaType.APPLICATION_JSON})
@Consumes({MediaType.APPLICATION_JSON})
public Response updateContact(@PathParam("proxy") String id, ContactObject co);

@GET
@Produces({MediaType.APPLICATION_JSON})
public Response getByCustId(@QueryParam("cid") String cid);

@GET
@Path("/{proxy}")
@Produces({MediaType.APPLICATION_JSON})
public Response getById(@PathParam("proxy") String id);

And it appears that all my tests work just fine.

Thanks,
David

@sapessi
Copy link
Collaborator

sapessi commented Mar 21, 2017

Is there a repo where we can take a look at the entire app? When I initialize my code with the @Path("{id}) only declared in the interface I get a 404 on the DELETE method, as if it couldn't find it.

On the 1st question, 403 is normally returned by API Gateway when you try to open a method that is not defined in the API. Would be good to see the API Gateway definition.

405 on the other hand, it's definitely not API Gateway unless you have explicitly defined that as a response code.

@davidmco65
Copy link
Author

davidmco65 commented Mar 21, 2017

Unfortunately, this is a private business app and I can't share all of the sources. I can share more of the code around this resource with a little editing.
Here's the API definition:
image

image

image

Complete (scrubbed) Resource interface:

package com.microstar.tap3.contact;

import javax.ws.rs.Consumes;
import javax.ws.rs.DELETE;
import javax.ws.rs.GET;
import javax.ws.rs.HEAD;
import javax.ws.rs.POST;
import javax.ws.rs.PUT;
import javax.ws.rs.Path;
import javax.ws.rs.PathParam;
import javax.ws.rs.Produces;
import javax.ws.rs.QueryParam;
import javax.ws.rs.core.MediaType;
import javax.ws.rs.core.Response;
import com.microstar.tap3.contact.api.ContactObject;

public interface ContactResource
{

	/*
	 * Returns nothing; just serves to keep the Lambda warm.
	 * <p>
	 * This method always returns immediately with an 200 OK status and no body.
	 * 
	 * @return	200 OK status with no body.
	 */
	@HEAD
	@Path("/ping")
	@Consumes({MediaType.APPLICATION_JSON})
	public Response ping(@QueryParam("numAdditionalInstancesToPing") int numAdditionalInstancesToPing);
	
	@DELETE
	@Consumes({MediaType.APPLICATION_JSON})
	@Produces({MediaType.APPLICATION_JSON})
	@Path("/{proxy}")
	public Response deleteContact(@PathParam("proxy") String id);

	@POST
	@Consumes({MediaType.APPLICATION_JSON})
	@Produces({MediaType.APPLICATION_JSON})
	public Response createContact(ContactObject co);

	@PUT
	@Path("/{proxy}")
	@Produces({MediaType.APPLICATION_JSON})
	@Consumes({MediaType.APPLICATION_JSON})
	public Response updateContact(@PathParam("proxy") String id, ContactObject co);

	@GET
//	@Path("/{cid}")
	@Produces({MediaType.APPLICATION_JSON})
	public Response getByCustId(@QueryParam("cid") String cid);

	@GET
	@Path("/{proxy}")
	@Produces({MediaType.APPLICATION_JSON})
	public Response getById(@PathParam("proxy") String id);

}

And complete (scrubbed) Resource impl:

import java.util.*;

import javax.ws.rs.Path;
import javax.ws.rs.core.Response;
import javax.ws.rs.core.Response.Status;

import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

@Path("/contacts")
public class ContactResourceImpl implements ContactResource
{
    private static final Logger 	LOG = LoggerFactory.getLogger(ContactResourceImpl.class);

    public Response deleteContact(String id)
    {
        return deleteContact(id, null);
    }

    /* (non-Javadoc)
     * @see com.microstar.tap3.contact.ContactResource#deleteContact(java.lang.String)
     */
    public Response deleteContact(String id, String cid)
    {
        LOG.debug("Entering contacts::DELETE /" + id);
        return Response.ok().build();
    }

    /* (non-Javadoc)
     * @see com.microstar.tap3.contact.ContactResource#createContact(com.microstar.tap3.contact.api.ContactObject)
     */
    public Response createContact(Map<String, String> co)
    {
        LOG.debug("Entering contacts::POST");
        return Response
                    .status(Status.CREATED)
                    .entity(co)
                    .build();
    }

    /* (non-Javadoc)
     * @see com.microstar.tap3.contact.ContactResource#updateContact(java.lang.String, com.microstar.tap3.contact.api.ContactObject)
     */
    public Response updateContact(String id, Map<String, String> co)
    {
        LOG.debug("Entering contacts::PUT /" + id);
        return Response
                    .ok()
                    .build();
    }

    /* (non-Javadoc)
     * @see com.microstar.tap3.contact.ContactResource#getByCustId(java.lang.String)
     */
    public Response getByCustId(String cid)
    {
        LOG.info("Entering contacts::GET ?cid=" + cid);
        return Response.ok()
                    .entity(Collections.EMPTY_MAP)
                    .build();
    }

    /* (non-Javadoc)
     * @see com.microstar.tap3.contact.ContactResource#getById(java.lang.String)
     */
    public Response getById(String id)
    {
        LOG.info("Entering contacts::GET /" + id);
        return Response
                    .ok()
                    .entity(Collections.EMPTY_MAP)
                    .build();
    }

    public Response ping(int numAdditionalInstancesToPing)
    {
        LOG.info("Entering ping::" + numAdditionalInstancesToPing);
        LOG.info("ContactResourceImpl: " + this);
        LOG.info("Thread: " + Thread.currentThread().getId());

        return Response
                .ok()
                .build();
    }
}

@sapessi
Copy link
Collaborator

sapessi commented Mar 21, 2017

Thanks Daniel,

You are getting a 403 on the POST because there isn't an HTTP method defined in the /contacts resource in API Gateway. The ANY method you have defined there in the {proxy+} resource will only handle calls to sub-resources of contacts, for example /contacts/123. To handle requests to the contacts resource your API should look like this:

/
|_ /contacts
   |_ANY -> Lambda (POST/GET calls to /contacts)
   |_/{proxy+}
      |_ANY -> Lambda (calls to /contacts/123 - could be the same Lambda function for both

This is a start. I'll look into the code today but the 405 sounds like something with Jersey. I'll let you know what I find.

@davidmco65
Copy link
Author

Oh wow! I didn't know you had to do that! Thanks so much! That helps a lot. I'll try that out very shortly.

David

@sapessi
Copy link
Collaborator

sapessi commented Mar 21, 2017

I also edited your comment to add an even more scrubbed version of the impl class.

@sapessi
Copy link
Collaborator

sapessi commented Mar 21, 2017

Assuming the API Gateway configuration fixes your 403 on the POST. I'm now trying to replicate the 405 on the DELETE method without much luck.

When I start the server in local with your latest code, it responds to the DELETE method as expected. Same when I use Lambda and API Gateway.

This is the class I'm using to run the local tests:

import org.glassfish.grizzly.http.server.HttpServer;
import org.glassfish.jersey.grizzly2.httpserver.GrizzlyHttpServerFactory;
import org.glassfish.jersey.jackson.JacksonFeature;
import org.glassfish.jersey.logging.LoggingFeature;
import org.glassfish.jersey.server.ResourceConfig;

import javax.ws.rs.core.UriBuilder;
import java.io.IOException;
import java.net.InetAddress;
import java.net.URI;
import java.net.UnknownHostException;

public class Service {
    public static void main(String[] args) throws IOException {
        System.out.println("Starting Embedded Jersey HTTPServer...\n");
        HttpServer httpServer = createHttpServer();

        httpServer.start();
    }

    private static HttpServer createHttpServer() throws IOException {
        ResourceConfig rc = new ResourceConfig()
                .packages("com.sapessi.testapi") // this is the package where I created your Contact class
                .register(JacksonFeature.class)
                .register(LoggingFeature.class);

        return GrizzlyHttpServerFactory.createHttpServer(getServerUri(), rc);
    }

    private static URI getServerUri() {
        return UriBuilder.fromUri("http://" + getLocalHostname() + "/").port(8085).build();
    }

    private static String getLocalHostname() {
        String hostName = "localhost";
        try {
            hostName = InetAddress.getLocalHost().getCanonicalHostName();
        } catch (UnknownHostException e) {
            e.printStackTrace();
        }
        return hostName;
    }
}

And this is my simple Lambda handler:

import com.amazonaws.serverless.proxy.internal.model.AwsProxyRequest;
import com.amazonaws.serverless.proxy.internal.model.AwsProxyResponse;
import com.amazonaws.serverless.proxy.jersey.JerseyLambdaContainerHandler;
import com.amazonaws.services.lambda.runtime.Context;
import com.amazonaws.services.lambda.runtime.RequestHandler;
import org.glassfish.jersey.jackson.JacksonFeature;
import org.glassfish.jersey.server.ResourceConfig;

public class LambdaHandler implements RequestHandler<AwsProxyRequest, AwsProxyResponse> {
    private ResourceConfig jerseyApplication = new ResourceConfig()
            .packages("com.sapessi.testapi")
            .register(JacksonFeature.class);
    private JerseyLambdaContainerHandler<AwsProxyRequest, AwsProxyResponse> handler
            = JerseyLambdaContainerHandler.getAwsProxyHandler(jerseyApplication);

    public AwsProxyResponse handleRequest(AwsProxyRequest awsProxyRequest, Context context) {
        return handler.proxy(awsProxyRequest, context);
    }
}

@davidmco65
Copy link
Author

So adding the additional method under the /contacts resource made everything work!

Not sure I totally understand why, but as long as it works, I'm happy.

I'd also be interested in contributing to this project if you are looking for developers/maintainers. Let me know.

Thanks,
David

@sapessi
Copy link
Collaborator

sapessi commented Mar 21, 2017

Thanks David, I'm currently working on issues #15 and #16. However, if there are other features you need in the library, feel free to open a new issue and send a pull request. We are always looking for feedback on the library.

I'm going to close this issue for now. thanks for all the help to debug this.

@sapessi sapessi closed this as completed Mar 21, 2017
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Projects
None yet
Development

No branches or pull requests

2 participants