How to make AWS API gateway to "understand" trailing slash?

Viewed 1785

I have an AWS API Gateway configured such that /auth method calls a Lambda. However, an existing product tries to call /auth/ with trailing slash and it ends up as error 404.

What can I do so that /auth/ URL goes to the same route as /auth in the API Gateway?

2 Answers

You could configure the route as ANY /{proxy+} so that any HTTP method (GET, POST, PATCH, DELETE) for any routes are directed to the same handler. Alternatively, you could also specify the HTTP method to narrow it down, like POST /{proxy+}.

So...

What can I do so that /auth/ URL goes to the same route as /auth in the API Gateway?

Technically speaking, this solves your problem, but now it is up to you to differentiate routes and know what to do.

As far as I know, this is the only way to achieve it with API Gateway, since according to some RFC out there routes "/auth" and "/auth/" are actually different routes and API Gateway complies with that RFC.

This is what I ended up doing (using the ANY /{proxy+}) and, if it is any help, this is the code I have to handle my routes and know what to do:

// Queue.ts

class Queue<T = any> {
  private items: T[];

  constructor(items?: T[]) {
    this.items = items || [];
  }

  get length(): number {
    return this.items.length;
  }

  enqueue(element: T): void {
    this.items.push(element);
  }

  dequeue(): T | undefined {
    return this.items.shift()!;
  }

  peek(): T | undefined {
    if (this.isEmpty()) return undefined;
    return this.items[0];
  }

  isEmpty() {
    return this.items.length == 0;
  }
}

export default Queue;
// PathMatcher.ts

import Queue from "./Queue";

type PathMatcherResult = {
  isMatch: boolean;
  namedParameters: Record<string, string>;
};

const NAMED_PARAMETER_REGEX = /(?!\w+:)\{(\w+)\}/;

class PathMatcher {
  static match(pattern: string, path: string): PathMatcherResult {
    const patternParts = new Queue<string>(this.trim(pattern).split("/"));
    const pathParts = new Queue<string>(this.trim(path).split("/"));

    const namedParameters: Record<string, string> = {};
    const noMatch = { isMatch: false, namedParameters: {} };

    if (patternParts.length !== pathParts.length) return noMatch;

    while (patternParts.length > 0) {
      const patternPart = patternParts.dequeue()!;
      const pathPart = pathParts.dequeue()!;

      if (patternPart === "*") continue;
      if (patternPart.toLowerCase() === pathPart.toLowerCase()) continue;
      if (NAMED_PARAMETER_REGEX.test(patternPart)) {
        const [name, value] = this.extractNamedParameter(patternPart, pathPart);
        namedParameters[name] = value;
        continue;
      }

      return noMatch;
    }

    return { isMatch: true, namedParameters };
  }

  private static trim(path: string) {
    return path.replace(/^[\s\/]+/, "").replace(/[\s\/]+$/, "");
  }

  private static extractNamedParameter(
    patternPart: string,
    pathPart: string
  ): [string, string] {
    const name = patternPart.replace(NAMED_PARAMETER_REGEX, "$1");
    let value = pathPart;
    if (value.includes(":")) value = value.substring(value.indexOf(":") + 1);
    return [name, value];
  }
}

export default PathMatcher;
export { PathMatcherResult };

Then, in my lambda handler, I do:

const httpMethod = event.requestContext.http.method.toUpperCase();
const currentRoute = `${httpMethod} ${event.rawPath}`;

// This will match both:
// GET /products/asdasdasdas
// GET /products/asdasdasdas/
const match = PathMatcher.match("GET /products/{id}", currentRoute);

if (match.isMatch) {
  // Here, the id parameter has been extracted for you
  const productId = match.namedParameters.id;
}

Of course you can build a registry of routes and their respective handler functions and automate that matching process and passing of parameters, but that is the easy part.

Turns out that the way to solve it is to configure the path like so (Terraform code from API Gateway config)

WAS

    "GET /auth" = {
      integration_type        = "AWS_PROXY"
      integration_http_method = "POST"
      payload_format_version  = "2.0"
      lambda_arn              = module.my-lambda.this_lambda_function_invoke_arn
    }

(this makes /auth work)

NOW

    "GET /auth/{proxy+}" = {
      integration_type        = "AWS_PROXY"
      integration_http_method = "POST"
      payload_format_version  = "2.0"
      lambda_arn              = module.my-lambda.this_lambda_function_invoke_arn
    }

(this makes /auth/ work and breaks /auth).

Related