7
votes

Example code is available on https://github.com/baumgarb/reverse-proxy-demo The README.md explains how you can re-produce the issue if you clone the repo.

I have an API Gateway and a downstream service which returns todos (TodosAPI). A client goes through the API Gateway to access the downstream service.

The API Gateway is leveraging the http-proxy-middleware package to proxy the requests. There are two implementations and the 2nd one is not working:

1. Global middleware in main.ts which kicks in on path /api/v1/...

This approach works perfectly fine and it proxies all requests to the downstream service no matter what http method (GET, PUT, ...).

import * as proxy from 'http-proxy-middleware';

app.use(
  '/api/v1/todos-api',
  proxy({
    target: 'http://localhost:8090/api',
    pathRewrite: {
      '/api/v1/todos-api': ''
    },
    secure: false,
    onProxyReq: (proxyReq, req, res) => {
      console.log(
        `[Global Functional Middlware]: Proxying ${req.method} request originally made to '${req.originalUrl}'...`
      );
    }
  })
);

2. NestMiddleware which is registered in the app module which kicks in on path /api/v2/...

This approach works fine for GET requests, but other http methods like PUT keep "hanging" and no response is ever received on the client. The problem seems to be that the controller in the downstream service is never invoked.

import * as proxy from 'http-proxy-middleware';

export class ReverseProxyMiddleware implements NestMiddleware {
  private proxy = proxy({
    target: 'http://localhost:8090/api',
    pathRewrite: {
      '/api/v2/todos-api': ''
    },
    secure: false,
    onProxyReq: (proxyReq, req, res) => {
      console.log(
        `[NestMiddleware]: Proxying ${req.method} request originally made to '${req.originalUrl}'...`
      );
    }
  });

  use(req: Request, res: Response, next: () => void) {
    this.proxy(req, res, next);
  }
}

And this middleware is registered as follows:

@Module({
  imports: [],
  controllers: [AppController],
  providers: [AppService]
})
export class AppModule implements NestModule {
  configure(consumer: import('@nestjs/common').MiddlewareConsumer) {
    consumer
      .apply(ReverseProxyMiddleware)
      .forRoutes({ path: 'v2/todos-api', method: RequestMethod.ALL });
  }
}
  • Running curl -X PUT -H "Content-Type: application/json" -d "{\"id\": 1, \"userId\": 1, \"title\": \"delectus aut autem - v1\", \"completed\": true}" http://localhost:8080/api/v1/todos-api/1 works perfectly fine
  • Running curl -X PUT -H "Content-Type: application/json" -d "{\"id\": 1, \"userId\": 1, \"title\": \"delectus aut autem - v2\", \"completed\": true}" http://localhost:8080/api/v2/todos-api/1 is having the issue where the controller in the downstream service is never invoked

The NestMiddleware is proxying the request (I can see a log line saying [NestMiddleware]: Proxying PUT request originally made to '/api/v2/todos-api/1'...) and the downstream service receives the request (I can see that from logging). But the downstream service does not invoke the controller / action and eventually never returns.

Has anyone any idea what I'm doing wrong here? Thanks a lot in advance!

2
I'll be honest, this isn't something really to put in a comment, but it's not really an answer so: I've tried looking into this and checked what's coming in through both proxies (the global and the Nest middleware one) and the reqs look identical. I have a feeling that there is some problem due to the underlying structure of the http-proxy-middleware package and how that middleware works. By default, Nest doesn't necessarily handle error middleware, and I have a feeling it is somehow dealing with that. This may need to be brought up as an issue on the github repository.Jay McDoniel
Thanks a lot for the response and the thorough analysis. When you say 'the reqs look identical': did you check the incoming requests on the downstream service side? If yes I think I'm lost. Because if they're identical on the downstream service side of things then I can not think of any explanation why the action in the downstream service wouldn't be invoked.baumgarb
Yeah, just did a simple console.log(req) in a global middleware on the server that was being proxied to and saw practically no differences. If that is the case, I think there is a some sort of post-request functionality the middleware tries to handle, but can't due to how Nest sets things up. Interesting idea would be to try this in an interceptor instead of a middleware and see if it works there or not.Jay McDoniel
Stupid question here -- "other than get" w/ your application/json content-type has me thinking it's a cors issue. Perhaps Nest isn't handling the cors error? Have you tried hitting v2 from postman?Schalton
Nope, no CORS issue here. No client with web context involved that would prevent cross domain requests. It's a simple API call done with curl. You can also test it with Postman or Insomnia, it's no different than with curl.baumgarb

2 Answers

6
votes

I've finally figured out the problem. It seems to be related to the body parser. If I change the API Gateway to turn off the body parser the request is forwarded successfully.

So the solution was to replace

const app = await NestFactory.create(AppModule);

with

const app = await NestFactory.create(AppModule, { bodyParser: false });

I've also created a branch issue-fixed in the Git repo where the fix is implemented.

4
votes

Set bodyParser: false when create Nest Application just fix the issue for endpoint we're proxying, it'll cause other endpoints (Eg: JWT localAuth) to be failed as they need body to be parsed.

The solution is to create a middleware as describe in this answer to disable bodyParser for specific endpoints you're proxying and enable it for the rest.