http-proxy-middleware icon indicating copy to clipboard operation
http-proxy-middleware copied to clipboard

Service async function on onProxyReq

Open pawelszczerbicki opened this issue 6 years ago • 30 comments
trafficstars

Having proxy configured with onProxyReq like:

proxy(gatewayUrl, {
  target: destinationUrl,
  pathRewrite: { [`^${gatewayUrl}`]: '' },
  changeOrigin: true,
  logProvider: () => logger,
  ...options,
  onProxyReq: async (proxyReq, req) => await addAuthHeader(proxyReq, req, redis),
});

Middleware is finishing request before promise is resolved. It is possible to service promise in proxy functions ?

pawelszczerbicki avatar Jan 03 '19 16:01 pawelszczerbicki

@pawelszczerbicki Having the same question. I will probably use node-http-proxy instead, as there's a documented example for this case.

julkue avatar Jan 24 '19 18:01 julkue

I was also looking at making external calls to add/verify authorization headers but came to the same conclusion. I'm guessing that a viable option is to have another middleware do the external call and have it mutate incoming request headers (would transfer over to onProxyReq). But it would be really nice to be able to do this within http-proxy-middleware having matched the route/pattern of the incoming request.

magnusp avatar Feb 18 '19 22:02 magnusp

Looks like there is some demand for async listeners. I'll consider implementation after some investigation.

@julmot Can you point to the documented node-http-proxy example?

@magnusp Think you can to achieve the same result by chaining middlewares. Assuming you use express you can write a authValidator and mount it in express like: app.use('/some/endpoint', authValidator, proxy)

chimurai avatar Feb 19 '19 17:02 chimurai

Im using it via webpack-dev-server which has a configuration property for http-proxy-middleware. The devserver also has a way to setup additional middleware via the "before" and "after" configuration properties. This leaves me having a choice of using my middleware+http-proxy-middleware in a "before" or only setting up my middleware in "before" and using the dedicated "proxy" configuration property. Im mostly mentioning this here for other people stumbling upon the issue and letting them see the options available. But async listeners is an awesome feature. I remember wanting it when implementing a chaos monkey where delays on the proxy were introduced randomly and configurably, at runtime.

magnusp avatar Feb 19 '19 20:02 magnusp

@chimurai I have to deal with promises in both proxyReq and proxyRes functions. Would be very helpful to have to have these functions support promises.

SreeBS avatar Feb 20 '19 11:02 SreeBS

+1 one for async

how can I achieve using the result of an async call there?

i am doing something like this

onst buildingConfProxyOptions = {
  target: TARGET_BASE_URL,
  changeOrigin: true,
  pathRewrite: {
    '^/building-conf': ''
  },
  async onProxyReq(proxyReq: any, req: any, res: any) {
    const token = req.headers['authorization'];
    const userSvc: UserService = Container.get(UserService);
    const result = await userSvc.verify(token); // Its ending here
// BUT I NEED TO USE RESULT HERE!

  },
  loglevel: 'debug'
};

montera82 avatar Feb 28 '19 20:02 montera82

For temporary workaround, I'm doing it like this with promise:

onProxyReq: (proxyReq, req, res) => {
  proxyReq.socket.pause();
  aPromise(a, b).then((c) => {
    proxyReq.setHeader('x-added', c);
    proxyReq.socket.resume();
  }).catch((err) => {
    console.error(err);
    res.sendStatus(500);
  });
},

Or with callback function:

onProxyReq: (proxyReq, req, res) => {
  proxyReq.socket.pause();
  aFunctionWithCallback(a, b, (err, c) => {
    if (err) {
      console.error(err);
      res.sendStatus(500);
      return;
    }
    proxyReq.setHeader('x-added', c);
    proxyReq.socket.resume();
  });
},

Is this plausible at all?

shian15810 avatar Mar 28 '19 07:03 shian15810

@shian15810 Did you try? Does it work?

boxcee avatar Mar 28 '19 17:03 boxcee

Yeah, it works! By plausible I mean does this really make sense?

shian15810 avatar Mar 28 '19 17:03 shian15810

Seems reasonable. I'm guessing that the proxyReq.socket should probably be aborted when an error occurs otherwise it could be left hanging until aborted by an timeout in either end.

magnusp avatar Mar 28 '19 17:03 magnusp

@magnusp Do you mean something like proxyReq.socket.end() after res.sendStatus(500)?

Or should it be better off if I get rid of res.sendStatus(500) and use proxyReq.socket.end('HTTP/1.1 500 Internal Server Error\r\n\r\n') instead?

Edited 1:

Just check the source code in express, express's res.sendStatus will trigger res.send within itself and in the end trigger node's http res.end.

Edited 2:

Just tested, socket.resume is not necessary when using res.sendStatus.

shian15810 avatar Mar 29 '19 04:03 shian15810

@shian15810 How did you set the header. When i try to set the header via proxyReq.setHeader('a','g'), i get an exception throwing

Cannot set header after they are sent to the client.

how did you get around that part? I followed the first promise example

Also not sure what the deal is but the res object has not sendStatus() method.....

Thanks!

darewreck54 avatar May 01 '19 09:05 darewreck54

@darewreck54 I was having this issue too, and after looking into the source code, at least for me, I couldn't find any way to circumvent this situation, even when using req.headers[customHeader].

But having custom header is essential to the functioning of my application. So finally I decided to change the whole architecture to microservice-based. Sorry, couldn't be much of your help.

If you don't need custom header for your application to be functional, the hack I proposed earlier is still a feasible way to get around the original issue.

Using req.headers[customHeader] won't give you that annoying error, but the header won't reach the upstream server anyway.

shian15810 avatar May 02 '19 09:05 shian15810

I actually end up creating my own express middleware that execute the promise and when its successful it will trigger next(). I'll store the updated header in the request and send it to the proxy middleware

darewreck54 avatar May 02 '19 10:05 darewreck54

@chimurai did you create a solution to this? Also if not can you provide an example of the authValidator you mentioned?

kavitalikesfajitas avatar Jul 24 '19 19:07 kavitalikesfajitas

For temporary workaround, I'm doing it like this with promise:

onProxyReq: (proxyReq, req, res) => {
  proxyReq.socket.pause();
  aPromise(a, b).then((c) => {
    proxyReq.setHeader('x-added', c);
    proxyReq.socket.resume();
  }).catch((err) => {
    console.error(err);
    res.sendStatus(500);
  });
},

Or with callback function:

onProxyReq: (proxyReq, req, res) => {
  proxyReq.socket.pause();
  aFunctionWithCallback(a, b, (err, c) => {
    if (err) {
      console.error(err);
      res.sendStatus(500);
      return;
    }
    proxyReq.setHeader('x-added', c);
    proxyReq.socket.resume();
  });
},

Is this plausible at all?

This works fine on onProxyReq but doesn't work on onProxyRes when trying to set the header. The header never reaches browser.

@julmot any links to the example in http-proxy will be great.

@chimurai any work around / solution on getting this working with onProxyRes?

mihir83in avatar Aug 28 '19 07:08 mihir83in

@shian15810 I somehow can't get it quite to work:

function onProxyReq(proxyReq, req, res) {
        proxyReq.socket.pause();
        setTimeout(function() {
                proxyReq.setHeader('X-AUTH-USERNAME', 'admin');
                proxyReq.socket.resume();
        }, 1000);
}

which results to:

throw new ERR_HTTP_HEADERS_SENT('set');
^
Error [ERR_HTTP_HEADERS_SENT]: Cannot set headers after they are sent to the client
    at ClientRequest.setHeader (_http_outgoing.js:470:11)

Is anything that I might be missing?

dpetrov avatar Feb 04 '20 20:02 dpetrov

So for the others that might be looking, I've ended up as well writing a middleware that stores the value into the req. For example:

app.use(function (req, res, next) {
  aFunctionWithCallback((err, res) => {
    req._username = res.username;
    next();
  });
});

and later on:

  "onProxyReq": function(proxyReq, req) {
    proxyReq.setHeader('X-AUTH-USERNAME', req._username);
  },

Probably not plausible at all, but it does work.

dpetrov avatar Feb 04 '20 20:02 dpetrov

Thanks @dpetrov this works here too. you dont even have to use onProxyReq, headers will get forwarded by the proxy later on

server.use((req, res, next) => {
  if (req.path === "/graphql" && req.method === "POST") {
    getSomethingAsync(req.session.id, (err, data) => {
      if (data) {
        req.headers["Authorization"] = `Bearer ${data.user.token}`
      }
      next()
    })
  } else {
    next()
  }
})

revolunet avatar Apr 17 '20 11:04 revolunet

no way @revolunet you are here too

kwiss avatar May 01 '20 00:05 kwiss

Are there any constraints in the library that would block a community PR for this @chimurai?

Was able to work around with a middleware that fires before my proxy middleware

Austio avatar May 29 '20 21:05 Austio

There was a PR last year on http-proxy for this. And without async support in handlers even though it might be somewhat trivial to implement, I didnt really get a warm fuzzy when I looked at the internals.

Ill just use a library with keep alive and call it good.

smileatom avatar Oct 02 '20 12:10 smileatom

@chimurai Steven bouw deze feature even erin man :)

we also want the async option, just before we stream back the results we want to alter header with async data.

i will checkout the mentioned node-http-proxy because it has selfHandleResponse option

maapteh avatar Nov 11 '20 16:11 maapteh

I tried with selfHandleResponse option, and it seems working in this middleware as well.

shihshen avatar Nov 19 '20 05:11 shihshen

@chimurai Steven, ik heb hem werkend :)

So i have this setup where i can still alter the response header AFTER i get the response from the proxied endpoint and do async stuff. In this sample i also included the fake server im mocking to (3001). Maybe nice for your recipe? Its setting response header on the response AFTER getting the response and do async stuff.

https://codesandbox.io/s/holy-resonance-yz552?file=/src/index.js

curl -I http://localhost:3000 gives now:

HTTP/1.1 200 OK
X-Powered-By: Express
mpth-2: da
Date: Thu, 19 Nov 2020 21:53:43 GMT
Connection: keep-alive

where mpth-2 is set async.

all i had to do was use selfHandleResponse: true, and proxyRes.pipe(res); just by digging in the code :(

thanks @powpowshen for telling it is possible, it made me look again :)

maapteh avatar Nov 19 '20 21:11 maapteh

感谢@dpetrov这里也行得通。你甚至不必使用, 头稍后会由代理转发onProxyReq

server.use((req, res, next) => {
  if (req.path === "/graphql" && req.method === "POST") {
    getSomethingAsync(req.session.id, (err, data) => {
      if (data) {
        req.headers["Authorization"] = `Bearer ${data.user.token}`
      }
      next()
    })
  } else {
    next()
  }
})

your answer is good

zhuibo66 avatar Aug 14 '21 13:08 zhuibo66

Just a note to anyone else looking - the posted workaround which pauses the socket seems to not work with a Websocket upgrade.

Edit:

I did find a workaround here - handle the server's upgrade and call proxy.upgrade from your async handler:

const server = expressApp.listen(8080);
server.on('upgrade', async (req: Request, socket: Socket, head: Buffer) => {
  someAsyncFunction()
    .then(() => {
      // do what you need with the results of the async function here
      if (upstreamProxy.upgrade) {
        upstreamProxy.upgrade(req, socket, head);
      }
    });
});

sarumont avatar Sep 02 '21 02:09 sarumont

@dpetrov and @revolunet you are lifesavers. This worked for me 👍 Thank you

server.use((req, res, next) => {
  if (req.path === "/graphql" && req.method === "POST") {
    getSomethingAsync(req.session.id, (err, data) => {
      if (data) {
        req.headers["Authorization"] = `Bearer ${data.user.token}`
      }
      next()
    })
  } else {
    next()
  }
})

blastshielddown avatar Nov 03 '21 00:11 blastshielddown