I have a provider as follows:
@Injectable()
export class GameServerProxyService {
private httpProxy: httpProxy;
constructor(@Inject(GameServerDetailsService) private gameServiceDetailsService: GameServerDetailsService) {
this.httpProxy = httpProxy.createProxyServer({});
this.httpProxy.on("proxyReq", (err,req,res)=>{
console.log("proxyReq");
});
this.httpProxy.on("proxyRes", (err,req,res)=>{
console.log("proxyRes");
});
this.httpProxy.on("error", (err,req,res)=>{
console.error(err);
});
}
proxyRequest(request: Request, response: Response){
console.log("proxyRequest");
this.httpProxy.web(request, response, {
target: "http://localhost:3100/",
changeOrigin: true,
}, (error) => {
console.log(error);
});
}
}
and a controller that has a route which uses this provider to proxy calls:
@Controller()
export class SsrApiGatewayController {
constructor(
@Inject(GameServerProxyService) private gameServerProxyService: GameServerProxyService
) { }
@All("game-server/*")
async proxyGameServerRequest(@Req() request: Request, @Res() response: Response) {
console.log("Proxying Request..!")
this.gameServerProxyService.proxyRequest(request, response);
}
}
When I sent a request to this route, my request stalls. I see the following logs:
Proxy Request..!
proxyReq
So the proxied request never reaches the response stage. I use Insomnia to send the requests. If I cancel the stalled request, I get an error on the server that I am proxying to (http://localhost:3100/):
[Nest] 6220 - 01/27/2022, 7:41:35 PM ERROR [ExceptionsHandler] request aborted
BadRequestError: request aborted
at IncomingMessage.onAborted (C:\******\node_modules\raw-body\index.js:231:10)
at IncomingMessage.emit (events.js:314:20)
at IncomingMessage.EventEmitter.emit (domain.js:483:12)
at abortIncoming (_http_server.js:533:9)
at socketOnClose (_http_server.js:526:3)
at Socket.emit (events.js:326:22)
at Socket.EventEmitter.emit (domain.js:483:12)
at TCP.<anonymous> (net.js:675:12)
So it is clear that my request is being proxied forward to the other server, but I am not getting back a response.
If I use http-proxy-middleware
instead, it works fine. I register the middleware like so:
async function bootstrap() {
const app = await NestFactory.create(SsrApiGatewayModule);
app.useGlobalPipes(
new ValidationPipe({
transform: true,
whitelist: true,
}),
);
app.use("/game-server/*",
createProxyMiddleware({
target: "http://localhost:3100",
changeOrigin: true,
})
);
var configService = app.get(ConfigService);
var commonConfig = configService.get<CommonConfig>(CommonKey);
await app.listen(commonConfig.port);
}
and the proxied requests process fine.
I would like to use my own provider as I will have a sophisticated router for the proxy target
which'll need to query some other providers that I plan to inject in. So I would like to 'stay inside' of the NestJS 'ecosystem' by writing my own provider.
I am not sure why my requests are stalling.
I have also tried to implement a NestJS middleware (so I can retain the ability to Inject providers into my middleware) and wrap the http-proxy-middleware
itself within my middleware instead of using http-proxy
myself. This fails in the same way.
Turns out the problem was that the BodyParser middleware was parsing the request body which "consumes" the underlying data stream. Then when my proxy code runs, it tries to proxy the fall and the request body with it, but is unable to do so as the data stream has been consumed. The proxied server waits indefinitely for the request data but it never arrives.
My solution was to write my own middleware that wraps both body parser and the proxy middleware. I decide which to use based on the request url - if URL starts with /game-server/
or ends with /game-server
, use proxy, else use body parser.
For completeness, here is the code:
Bootstrapping:
const app = await NestFactory.create(SsrApiGatewayModule, {
//We will manually invoke body-parser in the API Gateway Proxy Middleware.
bodyParser: false,
});
Root module
@Module({
//...
})
export class SsrApiGatewayModule implements NestModule {
configure(consumer: MiddlewareConsumer) {
consumer
.apply(ApiGatewayProxyMiddleware)
.forRoutes({ path: `*`, method: RequestMethod.ALL });
}
}
Middleware:
@Injectable()
export class ApiGatewayProxyMiddleware implements NestMiddleware {
private jsonBodyParser: RequestHandler;
private httpProxyMiddleware: RequestHandler;
private startWith: string;
private endWidth: string;
//You can inject stuff here.
constructor() {
this.jsonBodyParser = bodyParser.json();
//You can inject the config module to configure target, etc
this.httpProxyMiddleware = createProxyMiddleware({
target: "http://localhost:3000",
changeOrigin: true,
pathRewrite: {'^/game-server' : ''},
})
this.startWith = `/game-server/`;
this.endWidth = `/game-server`;
}
use(req: Request, res: Response, next: NextFunction) {
let pathMatch = req.path.startsWith(this.startWith);
if(!pathMatch) pathMatch = req.path.endsWith(this.endWidth);
if(pathMatch) {
console.log("Proxying request for path: " + req.path)
this.httpProxyMiddleware(req,res,next);
}
else {
//Only run body parser if we plan to handle the request here instead of proxying it https://stackoverflow.com/questions/70875743
console.log("Parsing request body for path: " + req.path);
this.jsonBodyParser(req, res, next);
}
}
}