UNPKG

next

Version:

The React Framework

113 lines (112 loc) 4.59 kB
import url from 'url'; import { stringifyQuery } from '../../server-route-utils'; import { Duplex } from 'stream'; import { DetachedPromise } from '../../../lib/detached-promise'; export async function proxyRequest(req, res, parsedUrl, upgradeHead, reqBody, proxyTimeout) { const { query } = parsedUrl; delete parsedUrl.query; parsedUrl.search = stringifyQuery(req, query); // Keep in mind that a WHATWG URL's hostname and the parsedUrl's hostname // are not strictly equal due to lowercasing, IDN translation, IPv4 and IPv6 normalization, etc. // We just make sure this is a valid URL since httpxy would only parse it // lazily on the `web()`/`ws()` call. const target = new URL(url.format(parsedUrl)); const { ProxyServer } = require('next/dist/compiled/httpxy'); const proxy = new ProxyServer({ target, changeOrigin: true, ignorePath: true, ws: true, // we limit proxy requests to 30s by default, in development // we don't time out WebSocket requests to allow proxying proxyTimeout: proxyTimeout === null ? undefined : proxyTimeout || 30000, headers: { 'x-forwarded-host': req.headers.host || '' } }); let finished = false; // httpxy does not properly detect a client disconnect in newer // versions of Node.js. This is caused because it only listens for the // `aborted` event on the our request object, but it also fully reads // and closes the request object. Node **will not** fire `aborted` when // the request is already closed. Listening for `close` on our response // object will detect the disconnect, and we can abort the proxy's // connection. proxy.on('proxyReq', (proxyReq)=>{ res.on('close', ()=>proxyReq.destroy()); }); proxy.on('proxyRes', (proxyRes)=>{ if (res.destroyed) { proxyRes.destroy(); } else { res.on('close', ()=>proxyRes.destroy()); } }); proxy.on('proxyRes', (proxyRes, innerReq, innerRes)=>{ const cleanup = (err)=>{ // cleanup event listeners to allow clean garbage collection proxyRes.removeListener('error', cleanup); proxyRes.removeListener('close', cleanup); innerRes.removeListener('error', cleanup); innerRes.removeListener('close', cleanup); // destroy all source streams to propagate the caught event backward innerReq.destroy(err); proxyRes.destroy(err); }; proxyRes.once('error', cleanup); proxyRes.once('close', cleanup); innerRes.once('error', cleanup); innerRes.once('close', cleanup); }); const detached = new DetachedPromise(); const onProxyError = (err)=>{ if (!finished) { finished = true; console.error(`Failed to proxy ${target}`, err); detached.reject(err); if (!res.destroyed) { if (!(res instanceof Duplex)) { res.statusCode = 500; } res.end('Internal Server Error'); } } }; proxy.on('error', onProxyError); // If upgrade head is present or the response is a Duplex stream, treat as // WebSocket request. if (upgradeHead || res instanceof Duplex) { proxy.on('proxyReqWs', (proxyReq)=>{ proxyReq.on('close', ()=>{ if (!finished) { finished = true; detached.resolve(true); } }); }); // The returned promise rejects on errors of the client socket (e.g. an // abrupt client disconnect) even when an `error` listener is registered. proxy.ws(req, res, {}, upgradeHead).catch(onProxyError); detached.resolve(true); } else { proxy.on('proxyReq', (proxyReq)=>{ proxyReq.on('close', ()=>{ if (!finished) { finished = true; detached.resolve(true); } }); }); // The returned promise rejects on errors of the client response stream // (e.g. an abrupt client disconnect) even when an `error` listener is // registered. proxy.web(req, res, { buffer: reqBody }).catch(onProxyError); } // When the proxy finishes proxying the request, shut down the proxy. return detached.promise.finally(()=>{ proxy.close(); }); } //# sourceMappingURL=proxy-request.js.map